From 900724f66049c43fee8584d9a94f947dabadb06c Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Mon, 19 Oct 2020 16:36:08 -0700 Subject: [PATCH 1/6] Updated version #. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b1895c6..58ede84 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "ImEX RPS", "author": "ImEX Systems Inc. ", "description": "ImEX RPS", - "version": "1.0.0", + "version": "1.0.1", "main": "electron/main.js", "homepage": "./", "dependencies": { From c277f6d32db9feece33e90f1d76e4a89ac34165b Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 20 Oct 2020 10:53:50 -0700 Subject: [PATCH 2/6] Added polling for watcher. --- electron/decoder/decoder.js | 10 ++-- electron/electron-store.js | 11 +++- electron/file-watcher/file-watcher.js | 20 ++++--- electron/ipc-main-handler.js | 17 ++++++ hasura/debug.log | 1 + .../1603204459690_run_sql_migration/down.yaml | 1 + .../1603204459690_run_sql_migration/up.yaml | 18 +++++++ .../1603204604474_run_sql_migration/down.yaml | 1 + .../1603204604474_run_sql_migration/up.yaml | 19 +++++++ .../atoms/data-label/data-label.atom.jsx | 26 +++++++++ .../part-type-converter.atom.jsx | 8 +-- .../watcher-polling.molecule.jsx | 54 +++++++++++++++++++ .../filepaths-list.organism.jsx | 1 - .../pages/settings/settings.page.jsx | 10 +++- src/ipc.types.js | 8 +++ src/ipc/ipc-renderer-handler.js | 5 ++ src/redux/application/application.actions.js | 4 ++ src/redux/application/application.reducer.js | 4 ++ .../application/application.selectors.js | 5 ++ src/redux/application/application.types.js | 1 + 20 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 hasura/migrations/1603204459690_run_sql_migration/down.yaml create mode 100644 hasura/migrations/1603204459690_run_sql_migration/up.yaml create mode 100644 hasura/migrations/1603204604474_run_sql_migration/down.yaml create mode 100644 hasura/migrations/1603204604474_run_sql_migration/up.yaml create mode 100644 src/components/atoms/data-label/data-label.atom.jsx create mode 100644 src/components/molecules/watcher-polling/watcher-polling.molecule.jsx diff --git a/electron/decoder/decoder.js b/electron/decoder/decoder.js index 21c873b..0199932 100644 --- a/electron/decoder/decoder.js +++ b/electron/decoder/decoder.js @@ -330,11 +330,11 @@ async function DecodeLinFile(extensionlessFilePath) { (jobline) => jobline.part_type && !jobline.db_ref.startsWith("900") && - !jobline.db_ref.toLowerCase().startsWith("urethane") && - !jobline.db_ref.toLowerCase().startsWith("wheel") && - !jobline.db_ref.toLowerCase().startsWith("hazardous") && - !jobline.db_ref.toLowerCase().startsWith("detail") && - !jobline.db_ref.toLowerCase().startsWith("clean") && + !jobline.line_desc.toLowerCase().startsWith("urethane") && + !jobline.line_desc.toLowerCase().startsWith("wheel") && + !jobline.line_desc.toLowerCase().startsWith("hazardous") && + !jobline.line_desc.toLowerCase().startsWith("detail") && + !jobline.line_desc.toLowerCase().startsWith("clean") && jobline.part_type.toUpperCase() !== "PAG" && jobline.part_type.toUpperCase() !== "PAS" && jobline.part_type.toUpperCase() !== "PASL" && diff --git a/electron/electron-store.js b/electron/electron-store.js index 9d59c9c..44a6a03 100644 --- a/electron/electron-store.js +++ b/electron/electron-store.js @@ -1,5 +1,14 @@ const Store = require("electron-store"); -const store = new Store({ defaults: { filePaths: [], accepted_ins_co: [] } }); +const store = new Store({ + defaults: { + filePaths: [], + accepted_ins_co: [], + polling: { + enabled: false, + pollingInterval: 100, + }, + }, +}); exports.store = store; diff --git a/electron/file-watcher/file-watcher.js b/electron/file-watcher/file-watcher.js index edb58d6..7f31647 100644 --- a/electron/file-watcher/file-watcher.js +++ b/electron/file-watcher/file-watcher.js @@ -7,35 +7,38 @@ const { store } = require("../electron-store"); const { NewNotification, } = require("../notification-wrapper/notification-wrapper"); - +const log = require("electron-log"); var watcher; async function StartWatcher() { const filePaths = store.get("filePaths").map((fp) => path.join(fp, "**.[eE][nN][vV]")) || []; - console.log("StartWatcher -> filePaths", filePaths); - + log.info("StartWatcher -> filePaths", 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 []; } if (watcher) { try { - console.log("Trying to close watcher - it already existed."); + log.info("Trying to close watcher - it already existed."); await watcher.close(); - console.log("Watcher closed successfully!"); + log.info("Watcher closed successfully!"); } catch (error) { - console.log("Error trying to close Watcher.", error); + log.error("Error trying to close Watcher.", error); } } watcher = chokidar.watch(filePaths, { //ignored: /[\/\\]\./, + usePolling: store.get("polling").enabled, persistent: true, ignoreInitial: true, awaitWriteFinish: { @@ -88,7 +91,7 @@ function onWatcherReady() { async function StopWatcher() { await watcher.close(); - console.log("Watcher stopped."); + log.info("Watcher stopped."); const b = BrowserWindow.getAllWindows()[0]; b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.stopSuccess); NewNotification({ @@ -111,12 +114,13 @@ async function HandleNewFile(path) { 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, diff --git a/electron/ipc-main-handler.js b/electron/ipc-main-handler.js index 6c69626..6a3da0d 100644 --- a/electron/ipc-main-handler.js +++ b/electron/ipc-main-handler.js @@ -17,3 +17,20 @@ ipcMain.on("test", async (event, object) => { ipcMain.on(ipcTypes.app.toMain.setAcceptableInsCoNm, (event, insCos) => { store.set("accepted_ins_co", insCos); }); + +ipcMain.on(ipcTypes.store.get, (event, key) => { + const val = store.get(key); + event.sender.send(ipcTypes.store.response, { [key]: val }); +}); + +ipcMain.on(ipcTypes.store.set, (event, key, val) => { + store.set(key, val); + + const st = store.get(); + event.sender.send(ipcTypes.store.response, st); +}); + +ipcMain.on(ipcTypes.store.getAll, (event, obj) => { + const val = store.get(); + event.sender.send(ipcTypes.store.response, val); +}); diff --git a/hasura/debug.log b/hasura/debug.log index ab51e52..3b38c55 100644 --- a/hasura/debug.log +++ b/hasura/debug.log @@ -1,2 +1,3 @@ [1014/195617.530:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) [1015/081931.328:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) +[1020/073641.000:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3) diff --git a/hasura/migrations/1603204459690_run_sql_migration/down.yaml b/hasura/migrations/1603204459690_run_sql_migration/down.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/hasura/migrations/1603204459690_run_sql_migration/down.yaml @@ -0,0 +1 @@ +[] diff --git a/hasura/migrations/1603204459690_run_sql_migration/up.yaml b/hasura/migrations/1603204459690_run_sql_migration/up.yaml new file mode 100644 index 0000000..7f5cbcc --- /dev/null +++ b/hasura/migrations/1603204459690_run_sql_migration/up.yaml @@ -0,0 +1,18 @@ +- args: + cascade: true + read_only: false + sql: "CREATE OR REPLACE FUNCTION public.search_jobs(search text, startdate date, + enddate date)\n RETURNS SETOF jobs\n LANGUAGE plpgsql\n STABLE\nAS $function$ + BEGIN if search = '' then return query\nselect *\nfrom jobs j;\nelse\n\nif (startDate + is null) or (endDate is null) then \nreturn query\nSELECT *\nFROM jobs j2\nWHERE + \n\nownr_fn ILIKE '%' || search || '%'\n or ownr_ln ILIKE '%' || search || + '%'\n \n or clm_no ILIKE '%' || search || '%'\nORDER BY \n clm_no ILIKE + '%' || search || '%'\n OR null,\n ownr_fn ILIKE '%' || search || '%'\n + \ OR NULL,\n ownr_ln ILIKE '%' || search || '%'\n OR NULL;\nelse \nreturn + query\nSELECT *\nFROM jobs j2\nWHERE \nclose_date between startDate and endDate + and close_date is not null and\n(\nownr_fn ILIKE '%' || search || '%'\n or + ownr_ln ILIKE '%' || search || '%'\n \n or clm_no ILIKE '%' || search || + '%')\n\nORDER BY \n clm_no ILIKE '%' || search || '%'\n OR null,\n ownr_fn + ILIKE '%' || search || '%'\n OR NULL,\n ownr_ln ILIKE '%' || search || + '%'\n OR NULL;\n\nend if;\n\n\nend if;\nEND $function$;" + type: run_sql diff --git a/hasura/migrations/1603204604474_run_sql_migration/down.yaml b/hasura/migrations/1603204604474_run_sql_migration/down.yaml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/hasura/migrations/1603204604474_run_sql_migration/down.yaml @@ -0,0 +1 @@ +[] diff --git a/hasura/migrations/1603204604474_run_sql_migration/up.yaml b/hasura/migrations/1603204604474_run_sql_migration/up.yaml new file mode 100644 index 0000000..6d0fe3b --- /dev/null +++ b/hasura/migrations/1603204604474_run_sql_migration/up.yaml @@ -0,0 +1,19 @@ +- args: + cascade: true + read_only: false + sql: "CREATE OR REPLACE FUNCTION public.search_jobs(search text, startdate date, + enddate date)\n RETURNS SETOF jobs\n LANGUAGE plpgsql\n STABLE\nAS $function$\nBEGIN + if search = '' and ((startDate is null) or (endDate is null)) then return query\nselect + *\nfrom jobs j;\nelse\n\nif (startDate is null) or (endDate is null) then \nreturn + query\nSELECT *\nFROM jobs j2\nWHERE \n\nownr_fn ILIKE '%' || search || '%'\n + \ or ownr_ln ILIKE '%' || search || '%'\n \n or clm_no ILIKE '%' || search + || '%'\nORDER BY \n clm_no ILIKE '%' || search || '%'\n OR null,\n ownr_fn + ILIKE '%' || search || '%'\n OR NULL,\n ownr_ln ILIKE '%' || search || + '%'\n OR NULL;\nelse \nreturn query\nSELECT *\nFROM jobs j2\nWHERE \nclose_date + between startDate and endDate and close_date is not null and\n(\nownr_fn ILIKE + '%' || search || '%'\n or ownr_ln ILIKE '%' || search || '%'\n \n or + clm_no ILIKE '%' || search || '%')\n\nORDER BY \n clm_no ILIKE '%' || search + || '%'\n OR null,\n ownr_fn ILIKE '%' || search || '%'\n OR NULL,\n + \ ownr_ln ILIKE '%' || search || '%'\n OR NULL;\n\nend if;\n\n\nend if;\nEND + $function$;" + type: run_sql diff --git a/src/components/atoms/data-label/data-label.atom.jsx b/src/components/atoms/data-label/data-label.atom.jsx new file mode 100644 index 0000000..e36060d --- /dev/null +++ b/src/components/atoms/data-label/data-label.atom.jsx @@ -0,0 +1,26 @@ +import React from "react"; + +export default function DataLabel({ + label, + hideIfNull, + children, + vertical, + visible = true, + ...props +}) { + if (!visible || (hideIfNull && !!!children)) return null; + + return ( +
+
{`${label}: `}
+
+ {children} +
+
+ ); +} diff --git a/src/components/atoms/part-type-converter/part-type-converter.atom.jsx b/src/components/atoms/part-type-converter/part-type-converter.atom.jsx index 624027a..c551e0a 100644 --- a/src/components/atoms/part-type-converter/part-type-converter.atom.jsx +++ b/src/components/atoms/part-type-converter/part-type-converter.atom.jsx @@ -1,14 +1,16 @@ export default (part_type) => { switch (part_type) { case "PAA": + case "PAL": + case "PAC": return "A/M"; case "PAE": return "Exist."; case "PAN": + case "PAP": return "OEM"; - case "PAL": - return "LKQ"; + default: - return "?"; + return part_type; } }; diff --git a/src/components/molecules/watcher-polling/watcher-polling.molecule.jsx b/src/components/molecules/watcher-polling/watcher-polling.molecule.jsx new file mode 100644 index 0000000..9a88bb9 --- /dev/null +++ b/src/components/molecules/watcher-polling/watcher-polling.molecule.jsx @@ -0,0 +1,54 @@ +import { InputNumber, 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 handlePollingToggle = (val) => { + ipcRenderer.send(ipcTypes.default.store.set, { "polling.enabled": val }); + }; + const handleIntervalChange = (val) => { + ipcRenderer.send(ipcTypes.default.store.set, { + "polling.pollingInterval": val, + }); + }; + + return ( +
+ + + + + + +
+ ); +} +export default connect( + mapStateToProps, + mapDispatchToProps +)(WatcherPollingMolecule); diff --git a/src/components/organisms/filepaths-list/filepaths-list.organism.jsx b/src/components/organisms/filepaths-list/filepaths-list.organism.jsx index 1e5c267..d883e12 100644 --- a/src/components/organisms/filepaths-list/filepaths-list.organism.jsx +++ b/src/components/organisms/filepaths-list/filepaths-list.organism.jsx @@ -21,7 +21,6 @@ export function FilePathsList({ watchedPaths }) { ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.filepathsGet); }, []); - console.log("watchedPaths", watchedPaths); return (
Watcher File Paths diff --git a/src/components/pages/settings/settings.page.jsx b/src/components/pages/settings/settings.page.jsx index fbed9b4..f23e018 100644 --- a/src/components/pages/settings/settings.page.jsx +++ b/src/components/pages/settings/settings.page.jsx @@ -1,10 +1,17 @@ import { Col, Row } from "antd"; -import React from "react"; +import React, { useEffect } from "react"; +import ipcTypes from "../../../ipc.types"; +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"; import WatcherManagerOrganism from "../../organisms/watcher-manager/watcher-manager.organism"; +const { ipcRenderer } = window; export default function SettingsPage() { + useEffect(() => { + ipcRenderer.send(ipcTypes.default.store.getAll); + }, []); + return (
@@ -13,6 +20,7 @@ export default function SettingsPage() { + diff --git a/src/ipc.types.js b/src/ipc.types.js index db36934..27ceace 100644 --- a/src/ipc.types.js +++ b/src/ipc.types.js @@ -9,6 +9,12 @@ exports.default = { setAcceptableInsCoNm: "setAcceptableInsCoNm", }, }, + store: { + get: "store__get", + getAll: "store_getAll", + set: "store_set", + response: "store_response", + }, fileWatcher: { toMain: { filepathsGet: "filewatcher__filepathsget", @@ -16,6 +22,7 @@ exports.default = { stop: "filewatcher__stop", addPath: "filewatcher__addPath", removePath: "filewatcher__removePath", + setPolling: "filewatcher__setPolling", }, toRenderer: { filepathsList: "filewatcher__filepathslist", @@ -23,6 +30,7 @@ exports.default = { startFailure: "filewatcher__start-failure", stopSuccess: "filewatcher__stop-success", error: "filewatcher__error", + getPolling: "filewatcher__getPolling", }, }, estimate: { diff --git a/src/ipc/ipc-renderer-handler.js b/src/ipc/ipc-renderer-handler.js index 75c956f..6fecb22 100644 --- a/src/ipc/ipc-renderer-handler.js +++ b/src/ipc/ipc-renderer-handler.js @@ -1,5 +1,6 @@ import ipcTypes from "../ipc.types"; import { + setSettings, setWatchedPaths, setWatcherStatus, } from "../redux/application/application.actions"; @@ -52,3 +53,7 @@ ipcRenderer.on( await UpsertEstimate(obj); } ); + +ipcRenderer.on(ipcTypes.default.store.response, (event, obj) => { + store.dispatch(setSettings(obj)); +}); diff --git a/src/redux/application/application.actions.js b/src/redux/application/application.actions.js index 6eb245d..ca179ea 100644 --- a/src/redux/application/application.actions.js +++ b/src/redux/application/application.actions.js @@ -38,3 +38,7 @@ export const setSelectedJobTargetPcSuccess = (pct) => ({ type: ApplicationActionTypes.SET_SELECTED_JOB_TARGET_PC_SUCCESS, payload: pct, }); +export const setSettings = (settingsObj) => ({ + type: ApplicationActionTypes.SET_SETTINGS, + payload: settingsObj, +}); diff --git a/src/redux/application/application.reducer.js b/src/redux/application/application.reducer.js index 2e25874..0814540 100644 --- a/src/redux/application/application.reducer.js +++ b/src/redux/application/application.reducer.js @@ -5,6 +5,7 @@ const INITIAL_STATE = { watcherError: null, selectedJobId: null, selectedJobTargetPc: 100, + settings: {}, }; const applicationReducer = (state = INITIAL_STATE, action) => { @@ -41,6 +42,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => { }; case ApplicationActionTypes.SET_SELECTED_JOB_ID: return { ...state, selectedJobId: action.payload }; + case ApplicationActionTypes.SET_SETTINGS: + return { ...state, settings: { ...state.settings, ...action.payload } }; + default: return state; } diff --git a/src/redux/application/application.selectors.js b/src/redux/application/application.selectors.js index db993f7..beafc9f 100644 --- a/src/redux/application/application.selectors.js +++ b/src/redux/application/application.selectors.js @@ -26,3 +26,8 @@ export const selectSelectedJobTargetPc = createSelector( [selectApplication], (application) => application.selectedJobTargetPc ); + +export const selectSettings = createSelector( + [selectApplication], + (application) => application.settings +); diff --git a/src/redux/application/application.types.js b/src/redux/application/application.types.js index bac9945..3a73825 100644 --- a/src/redux/application/application.types.js +++ b/src/redux/application/application.types.js @@ -7,5 +7,6 @@ const ApplicationActionTypes = { SET_SELECTED_JOB_ID: "SET_SELECTED_JOB_ID", SET_SELECTED_JOB_TARGET_PC: "SET_SELECTED_JOB_TARGET_PC", SET_SELECTED_JOB_TARGET_PC_SUCCESS: "SET_SELECTED_JOB_TARGET_PC_SUCCESS", + SET_SETTINGS: "SET_SETTINGS", }; export default ApplicationActionTypes; From 4290c8c4979f2b7592e3ef715022998dd08c11fd Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 20 Oct 2020 11:20:14 -0700 Subject: [PATCH 3/6] Added job line ignore. --- electron/decoder/decoder.js | 48 ++++++++++++------- .../down.yaml | 5 ++ .../up.yaml | 6 +++ .../down.yaml | 39 +++++++++++++++ .../up.yaml | 40 ++++++++++++++++ .../down.yaml | 39 +++++++++++++++ .../up.yaml | 40 ++++++++++++++++ .../down.yaml | 38 +++++++++++++++ .../up.yaml | 39 +++++++++++++++ hasura/migrations/metadata.yaml | 3 ++ .../ignore-job-line/ignore-job-line.atom.jsx | 24 ++++++++++ .../jobs-parts-graph.atom.jsx | 20 ++++---- .../jobs-lines-table.molecule.jsx | 43 +++++++++++++++-- .../jobs-targets-stats.molecule.jsx | 28 ++++++----- src/graphql/joblines.queries.js | 23 +++++++++ src/graphql/jobs.queries.js | 17 +------ 16 files changed, 394 insertions(+), 58 deletions(-) create mode 100644 hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/down.yaml create mode 100644 hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/up.yaml create mode 100644 hasura/migrations/1603216471209_update_permission_user_public_table_joblines/down.yaml create mode 100644 hasura/migrations/1603216471209_update_permission_user_public_table_joblines/up.yaml create mode 100644 hasura/migrations/1603216476135_update_permission_user_public_table_joblines/down.yaml create mode 100644 hasura/migrations/1603216476135_update_permission_user_public_table_joblines/up.yaml create mode 100644 hasura/migrations/1603216480562_update_permission_user_public_table_joblines/down.yaml create mode 100644 hasura/migrations/1603216480562_update_permission_user_public_table_joblines/up.yaml create mode 100644 src/components/atoms/ignore-job-line/ignore-job-line.atom.jsx create mode 100644 src/graphql/joblines.queries.js diff --git a/electron/decoder/decoder.js b/electron/decoder/decoder.js index 0199932..5fc5b74 100644 --- a/electron/decoder/decoder.js +++ b/electron/decoder/decoder.js @@ -326,28 +326,27 @@ async function DecodeLinFile(extensionlessFilePath) { } ); }) - .filter( - (jobline) => - jobline.part_type && - !jobline.db_ref.startsWith("900") && - !jobline.line_desc.toLowerCase().startsWith("urethane") && - !jobline.line_desc.toLowerCase().startsWith("wheel") && - !jobline.line_desc.toLowerCase().startsWith("hazardous") && - !jobline.line_desc.toLowerCase().startsWith("detail") && - !jobline.line_desc.toLowerCase().startsWith("clean") && - jobline.part_type.toUpperCase() !== "PAG" && - jobline.part_type.toUpperCase() !== "PAS" && - jobline.part_type.toUpperCase() !== "PASL" && - jobline.part_type.toUpperCase() !== "PAE" && - jobline.glass_flag === false - ) + // .filter( + // (jobline) => + // jobline.part_type && + // !jobline.db_ref.startsWith("900") && + // !jobline.line_desc.toLowerCase().startsWith("urethane") && + // !jobline.line_desc.toLowerCase().startsWith("wheel") && + // !jobline.line_desc.toLowerCase().startsWith("hazardous") && + // !jobline.line_desc.toLowerCase().startsWith("detail") && + // !jobline.line_desc.toLowerCase().startsWith("clean") && + // jobline.part_type.toUpperCase() !== "PAG" && + // jobline.part_type.toUpperCase() !== "PAS" && + // jobline.part_type.toUpperCase() !== "PASL" && + // jobline.part_type.toUpperCase() !== "PAE" && + // jobline.glass_flag === false + // ) .map((jobline) => { if ( (jobline.db_price === null || jobline.db_price === 0) && !!jobline.act_price && jobline.act_price > 0 ) { - console.log(1, jobline.line_desc, jobline.db_price, jobline.act_price); log.info( "DB Price null/lower than act price", jobline.line_desc, @@ -368,10 +367,25 @@ async function DecodeLinFile(extensionlessFilePath) { jobline.db_price, jobline.act_price ); - console.log(2, jobline.line_desc, jobline.db_price, jobline.act_price); jobline.db_price = jobline.act_price; } + if ( + !jobline.part_type || + jobline.db_ref.startsWith("900") || + jobline.line_desc.toLowerCase().startsWith("urethane") || + jobline.line_desc.toLowerCase().startsWith("wheel") || + jobline.line_desc.toLowerCase().startsWith("hazardous") || + jobline.line_desc.toLowerCase().startsWith("detail") || + jobline.line_desc.toLowerCase().startsWith("clean") || + jobline.part_type.toUpperCase() === "PAG" || + jobline.part_type.toUpperCase() === "PAS" || + jobline.part_type.toUpperCase() === "PASL" || + jobline.part_type.toUpperCase() === "PAE" || + jobline.glass_flag === true + ) + jobline.ignore = true; + delete jobline.glass_flag; return jobline; }); diff --git a/hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/down.yaml b/hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/down.yaml new file mode 100644 index 0000000..155b5e1 --- /dev/null +++ b/hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/down.yaml @@ -0,0 +1,5 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."joblines" DROP COLUMN "ignore"; + type: run_sql diff --git a/hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/up.yaml b/hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/up.yaml new file mode 100644 index 0000000..253741d --- /dev/null +++ b/hasura/migrations/1603216465074_alter_table_public_joblines_add_column_ignore/up.yaml @@ -0,0 +1,6 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."joblines" ADD COLUMN "ignore" boolean NOT NULL DEFAULT + false; + type: run_sql diff --git a/hasura/migrations/1603216471209_update_permission_user_public_table_joblines/down.yaml b/hasura/migrations/1603216471209_update_permission_user_public_table_joblines/down.yaml new file mode 100644 index 0000000..1b9527e --- /dev/null +++ b/hasura/migrations/1603216471209_update_permission_user_public_table_joblines/down.yaml @@ -0,0 +1,39 @@ +- args: + role: user + table: + name: joblines + schema: public + type: drop_insert_permission +- args: + permission: + backend_only: false + check: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + columns: + - act_price + - created_at + - db_price + - db_ref + - id + - jobid + - line_desc + - line_ind + - line_no + - oem_partno + - part_qty + - part_type + - price_diff + - price_diff_pc + - unq_seq + - updated_at + set: {} + role: user + table: + name: joblines + schema: public + type: create_insert_permission diff --git a/hasura/migrations/1603216471209_update_permission_user_public_table_joblines/up.yaml b/hasura/migrations/1603216471209_update_permission_user_public_table_joblines/up.yaml new file mode 100644 index 0000000..86afc84 --- /dev/null +++ b/hasura/migrations/1603216471209_update_permission_user_public_table_joblines/up.yaml @@ -0,0 +1,40 @@ +- args: + role: user + table: + name: joblines + schema: public + type: drop_insert_permission +- args: + permission: + backend_only: false + check: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + columns: + - act_price + - created_at + - db_price + - db_ref + - id + - ignore + - jobid + - line_desc + - line_ind + - line_no + - oem_partno + - part_qty + - part_type + - price_diff + - price_diff_pc + - unq_seq + - updated_at + set: {} + role: user + table: + name: joblines + schema: public + type: create_insert_permission diff --git a/hasura/migrations/1603216476135_update_permission_user_public_table_joblines/down.yaml b/hasura/migrations/1603216476135_update_permission_user_public_table_joblines/down.yaml new file mode 100644 index 0000000..518eff7 --- /dev/null +++ b/hasura/migrations/1603216476135_update_permission_user_public_table_joblines/down.yaml @@ -0,0 +1,39 @@ +- args: + role: user + table: + name: joblines + schema: public + type: drop_select_permission +- args: + permission: + allow_aggregations: false + columns: + - act_price + - created_at + - db_price + - db_ref + - id + - jobid + - line_desc + - line_ind + - line_no + - oem_partno + - part_qty + - part_type + - price_diff + - price_diff_pc + - unq_seq + - updated_at + computed_fields: [] + filter: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + role: user + table: + name: joblines + schema: public + type: create_select_permission diff --git a/hasura/migrations/1603216476135_update_permission_user_public_table_joblines/up.yaml b/hasura/migrations/1603216476135_update_permission_user_public_table_joblines/up.yaml new file mode 100644 index 0000000..873ee04 --- /dev/null +++ b/hasura/migrations/1603216476135_update_permission_user_public_table_joblines/up.yaml @@ -0,0 +1,40 @@ +- args: + role: user + table: + name: joblines + schema: public + type: drop_select_permission +- args: + permission: + allow_aggregations: false + columns: + - act_price + - created_at + - db_price + - db_ref + - id + - ignore + - jobid + - line_desc + - line_ind + - line_no + - oem_partno + - part_qty + - part_type + - price_diff + - price_diff_pc + - unq_seq + - updated_at + computed_fields: [] + filter: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + role: user + table: + name: joblines + schema: public + type: create_select_permission diff --git a/hasura/migrations/1603216480562_update_permission_user_public_table_joblines/down.yaml b/hasura/migrations/1603216480562_update_permission_user_public_table_joblines/down.yaml new file mode 100644 index 0000000..f753d5a --- /dev/null +++ b/hasura/migrations/1603216480562_update_permission_user_public_table_joblines/down.yaml @@ -0,0 +1,38 @@ +- args: + role: user + table: + name: joblines + schema: public + type: drop_update_permission +- args: + permission: + columns: + - act_price + - created_at + - db_price + - db_ref + - id + - jobid + - line_desc + - line_ind + - line_no + - oem_partno + - part_qty + - part_type + - price_diff + - price_diff_pc + - unq_seq + - updated_at + filter: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + set: {} + role: user + table: + name: joblines + schema: public + type: create_update_permission diff --git a/hasura/migrations/1603216480562_update_permission_user_public_table_joblines/up.yaml b/hasura/migrations/1603216480562_update_permission_user_public_table_joblines/up.yaml new file mode 100644 index 0000000..5f6a028 --- /dev/null +++ b/hasura/migrations/1603216480562_update_permission_user_public_table_joblines/up.yaml @@ -0,0 +1,39 @@ +- args: + role: user + table: + name: joblines + schema: public + type: drop_update_permission +- args: + permission: + columns: + - act_price + - created_at + - db_price + - db_ref + - id + - ignore + - jobid + - line_desc + - line_ind + - line_no + - oem_partno + - part_qty + - part_type + - price_diff + - price_diff_pc + - unq_seq + - updated_at + filter: + job: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + set: {} + role: user + table: + name: joblines + schema: public + type: create_update_permission diff --git a/hasura/migrations/metadata.yaml b/hasura/migrations/metadata.yaml index 2942282..c3aca2f 100644 --- a/hasura/migrations/metadata.yaml +++ b/hasura/migrations/metadata.yaml @@ -89,6 +89,7 @@ tables: - db_price - db_ref - id + - ignore - jobid - line_desc - line_ind @@ -110,6 +111,7 @@ tables: - db_price - db_ref - id + - ignore - jobid - line_desc - line_ind @@ -137,6 +139,7 @@ tables: - db_price - db_ref - id + - ignore - jobid - line_desc - line_ind diff --git a/src/components/atoms/ignore-job-line/ignore-job-line.atom.jsx b/src/components/atoms/ignore-job-line/ignore-job-line.atom.jsx new file mode 100644 index 0000000..b9e2cdc --- /dev/null +++ b/src/components/atoms/ignore-job-line/ignore-job-line.atom.jsx @@ -0,0 +1,24 @@ +import { useMutation } from "@apollo/client"; +import { message, Switch } from "antd"; +import React, { useState } from "react"; +import { UPDATE_JOB_LINE } from "../../../graphql/joblines.queries"; +const { log } = window; + +export default function IgnoreJobLineAtom({ ignore, lineId }) { + const [updateJobLine] = useMutation(UPDATE_JOB_LINE); + const [loading, setLoading] = useState(false); + const handleChange = async (checked) => { + setLoading(true); + const result = await updateJobLine({ + variables: { lineId: lineId, line: { ignore: checked } }, + }); + if (result.errors) { + message.error("Error updating line."); + log.error("Error updating job.", result.errors); + } else { + } + setLoading(false); + }; + + return ; +} diff --git a/src/components/atoms/jobs-parts-graph/jobs-parts-graph.atom.jsx b/src/components/atoms/jobs-parts-graph/jobs-parts-graph.atom.jsx index 358d889..ff3a765 100644 --- a/src/components/atoms/jobs-parts-graph/jobs-parts-graph.atom.jsx +++ b/src/components/atoms/jobs-parts-graph/jobs-parts-graph.atom.jsx @@ -12,17 +12,19 @@ export default function JobPartsGraphAtom({ const data = useMemo(() => { if (!job) return []; - const sums = job.joblines.reduce((acc, val) => { - if (!acc[val.part_type]) { - acc[val.part_type] = Dinero(); - } + const sums = job.joblines + .filter((j) => !j.ignore) + .reduce((acc, val) => { + if (!acc[val.part_type]) { + acc[val.part_type] = Dinero(); + } - acc[val.part_type] = acc[val.part_type].add( - Dinero({ amount: Math.round((val[price] || 0) * 100) }) - ); + acc[val.part_type] = acc[val.part_type].add( + Dinero({ amount: Math.round((val[price] || 0) * 100) }) + ); - return acc; - }, {}); + return acc; + }, {}); return Object.keys(sums).map((key) => { return { diff --git a/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx b/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx index 526961a..2749b7a 100644 --- a/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx +++ b/src/components/molecules/jobs-lines-table/jobs-lines-table.molecule.jsx @@ -1,16 +1,19 @@ -import { Table } from "antd"; -import React from "react"; +import { Input, Table } from "antd"; +import React, { useState } from "react"; 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"; import PriceDiffPcFormatterAtom from "../../atoms/price-diff-pc-formatter/price-diff-pc-formatter.atom"; export default function JobLinesTableMolecule({ loading, job }) { + const [searchText, setSearchText] = useState(""); + const { joblines } = job; const columns = [ { title: "#", - dataIndex: "unq_seq", - key: "unq_seq", + dataIndex: "line_no", + key: "line_no", }, { title: "S#", @@ -73,17 +76,47 @@ export default function JobLinesTableMolecule({ loading, job }) { /> ), }, + { + title: "Ignore?", + dataIndex: "ignore", + key: "ignore", + filters: [ + { text: "True", value: true }, + { text: "False", value: false }, + ], + onFilter: (value, record) => value === record.ignore, + render: (text, record) => ( + + ), + }, ]; + const data = + searchText !== "" + ? joblines.filter((j) => + j.line_desc.toLowerCase().includes(searchText.toLowerCase()) + ) + : joblines; + return (
( + { + setSearchText(val); + }} + enterButton + allowClear + /> + )} columns={columns} rowKey="id" loading={loading} size="small" pagination={false} - dataSource={joblines} + dataSource={data} scroll={{ x: true, y: "20rem", diff --git a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx index b5dc058..82b53ab 100644 --- a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx +++ b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx @@ -26,15 +26,17 @@ export function JobsTargetsStatsMolecule({ if (!job) { return 0; } - return job.joblines.reduce((acc, val) => { - if (val.price_diff > 0) { - return acc.add( - Dinero({ amount: Math.round((val.price_diff || 0) * 100) }) - ); - } else { - return acc; - } - }, Dinero()); + return job.joblines + .filter((j) => !j.ignore) + .reduce((acc, val) => { + if (val.price_diff > 0) { + return acc.add( + Dinero({ amount: Math.round((val.price_diff || 0) * 100) }) + ); + } else { + return acc; + } + }, Dinero()); }, [job]); const currentRpsPc = useMemo(() => { @@ -42,9 +44,11 @@ export function JobsTargetsStatsMolecule({ if (!job) { return 0; } - const dbPriceSum = job.joblines.reduce((acc, val) => { - return acc + val.db_price; - }, 0); + const dbPriceSum = job.joblines + .filter((j) => !j.ignore) + .reduce((acc, val) => { + return acc + val.db_price; + }, 0); return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1); }, [job, currentRpsDollars]); diff --git a/src/graphql/joblines.queries.js b/src/graphql/joblines.queries.js new file mode 100644 index 0000000..8f1879a --- /dev/null +++ b/src/graphql/joblines.queries.js @@ -0,0 +1,23 @@ +import gql from "graphql-tag"; + +export const UPDATE_JOB_LINE = gql` + mutation UPDATE_JOB_LINE($lineId: uuid!, $line: joblines_set_input!) { + update_joblines(where: { id: { _eq: $lineId } }, _set: $line) { + returning { + id + line_no + act_price + db_price + line_desc + line_ind + oem_partno + part_qty + part_type + unq_seq + price_diff + price_diff_pc + ignore + } + } + } +`; diff --git a/src/graphql/jobs.queries.js b/src/graphql/jobs.queries.js index 0528fbf..73be1ad 100644 --- a/src/graphql/jobs.queries.js +++ b/src/graphql/jobs.queries.js @@ -10,21 +10,6 @@ export const INSERT_NEW_JOB = gql` } `; -// on_conflict: { -// constraint: jobs_clm_no_bodyshopid_key -// update_columns: [ -// ins_co_nm -// clm_no -// clm_total -// ownr_ln -// ownr_fn -// v_vin -// v_make_desc -// v_model_desc -// v_type -// ] -// } - export const QUERY_ALL_JOBS_PAGINATED = gql` query QUERY_ALL_JOBS_PAGINATED( $offset: Int @@ -106,6 +91,7 @@ export const QUERY_JOB_BY_PK = gql` close_date joblines(order_by: { line_no: asc }) { id + line_no act_price db_price line_desc @@ -116,6 +102,7 @@ export const QUERY_JOB_BY_PK = gql` unq_seq price_diff price_diff_pc + ignore } } } From 045346ce4864892da019668fc33f5b18c814c05d Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 20 Oct 2020 13:55:35 -0700 Subject: [PATCH 4/6] WIP for Reporting. Pulled out calculations to utility functions. --- .../jobs-targets-stats.molecule.jsx | 40 ++++--------- .../reporting-dates.molecule.jsx | 44 ++++++++++++++ .../sider-sign-out.molecule.jsx | 4 +- .../sider-menu/sider-menu.organism.jsx | 4 ++ .../pages/reporting/reporting.page.jsx | 10 ++++ src/components/pages/routes/routes.page.jsx | 9 ++- .../pages/settings/settings.page.jsx | 8 --- src/graphql/reporting.queries.js | 47 +++++++++++++++ src/redux/application/application.sagas.js | 24 ++++---- src/redux/reporting/reporting.actions.js | 24 ++++++++ src/redux/reporting/reporting.reducer.js | 23 ++++++++ src/redux/reporting/reporting.sagas.js | 57 +++++++++++++++++++ src/redux/reporting/reporting.selectors.js | 49 ++++++++++++++++ src/redux/reporting/reporting.types.js | 8 +++ src/redux/root.reducer.js | 4 +- src/redux/root.saga.js | 4 +- src/util/CalculateJobRps.js | 31 ++++++++++ src/util/GetJobTarget.js | 12 ++++ 18 files changed, 345 insertions(+), 57 deletions(-) create mode 100644 src/components/molecules/reporting-dates/reporting-dates.molecule.jsx create mode 100644 src/components/pages/reporting/reporting.page.jsx create mode 100644 src/graphql/reporting.queries.js create mode 100644 src/redux/reporting/reporting.actions.js create mode 100644 src/redux/reporting/reporting.reducer.js create mode 100644 src/redux/reporting/reporting.sagas.js create mode 100644 src/redux/reporting/reporting.selectors.js create mode 100644 src/redux/reporting/reporting.types.js create mode 100644 src/util/CalculateJobRps.js create mode 100644 src/util/GetJobTarget.js diff --git a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx index 82b53ab..2c7008e 100644 --- a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx +++ b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx @@ -1,9 +1,12 @@ import { Skeleton, Statistic } from "antd"; -import Dinero from "dinero.js"; -import React, { useMemo } from "react"; +import React, { useCallback } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectSelectedJobTargetPc } from "../../../redux/application/application.selectors"; +import { + CalculateJobRpsDollars, + CalculateJobRpsPc, +} from "../../../util/CalculateJobRps"; import ErrorResultAtom from "../../atoms/error-result/error-result.atom"; const mapStateToProps = createStructuredSelector({ @@ -22,35 +25,12 @@ export function JobsTargetsStatsMolecule({ job, selectedJobTargetPc, }) { - const currentRpsDollars = useMemo(() => { - if (!job) { - return 0; - } - return job.joblines - .filter((j) => !j.ignore) - .reduce((acc, val) => { - if (val.price_diff > 0) { - return acc.add( - Dinero({ amount: Math.round((val.price_diff || 0) * 100) }) - ); - } else { - return acc; - } - }, Dinero()); - }, [job]); + const currentRpsDollars = useCallback(CalculateJobRpsDollars(job), [job]); - const currentRpsPc = useMemo(() => { - //TODO Redo this to do total of db price - act price / db price - if (!job) { - return 0; - } - const dbPriceSum = job.joblines - .filter((j) => !j.ignore) - .reduce((acc, val) => { - return acc + val.db_price; - }, 0); - return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1); - }, [job, currentRpsDollars]); + const currentRpsPc = useCallback(CalculateJobRpsPc(job, currentRpsDollars), [ + job, + currentRpsDollars, + ]); if (loading) return ; if (!job) return ; diff --git a/src/components/molecules/reporting-dates/reporting-dates.molecule.jsx b/src/components/molecules/reporting-dates/reporting-dates.molecule.jsx new file mode 100644 index 0000000..b908a9a --- /dev/null +++ b/src/components/molecules/reporting-dates/reporting-dates.molecule.jsx @@ -0,0 +1,44 @@ +import { Button, DatePicker, Form } from "antd"; +import React from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { queryReportingData } from "../../../redux/reporting/reporting.actions"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser +}); +const mapDispatchToProps = (dispatch) => ({ + queryReportingData: (dates) => dispatch(queryReportingData(dates)), +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ReportingDatesMolecule); + +export function ReportingDatesMolecule({ queryReportingData }) { + const [form] = Form.useForm(); + + const handleFinish = (values) => { + console.log("values", values); + queryReportingData({ + startDate: values.dateRange[0], + endDate: values.dateRange[1], + }); + }; + + return ( +
+
+ + + + +
+ + ); +} diff --git a/src/components/molecules/sider-sign-out/sider-sign-out.molecule.jsx b/src/components/molecules/sider-sign-out/sider-sign-out.molecule.jsx index daed0b4..9e50dd6 100644 --- a/src/components/molecules/sider-sign-out/sider-sign-out.molecule.jsx +++ b/src/components/molecules/sider-sign-out/sider-sign-out.molecule.jsx @@ -1,4 +1,4 @@ -import { UserOutlined } from "@ant-design/icons"; +import { LogoutOutlined } from "@ant-design/icons"; import { Menu } from "antd"; import React from "react"; import { connect } from "react-redux"; @@ -11,7 +11,7 @@ const mapDispatchToProps = (dispatch) => ({ export function SiderSignOut({ signOutStart, ...restProps }) { return ( } + icon={} {...restProps} onClick={() => signOutStart()} > diff --git a/src/components/organisms/sider-menu/sider-menu.organism.jsx b/src/components/organisms/sider-menu/sider-menu.organism.jsx index 7c02dd4..e82410e 100644 --- a/src/components/organisms/sider-menu/sider-menu.organism.jsx +++ b/src/components/organisms/sider-menu/sider-menu.organism.jsx @@ -2,6 +2,7 @@ import { PieChartOutlined, SettingFilled, CloseOutlined, + BarChartOutlined, } from "@ant-design/icons"; import { Menu } from "antd"; import React from "react"; @@ -19,6 +20,9 @@ export default function SiderMenuOrganism() { }> Jobs + }> + Reporting + }> Settings diff --git a/src/components/pages/reporting/reporting.page.jsx b/src/components/pages/reporting/reporting.page.jsx new file mode 100644 index 0000000..73a57d9 --- /dev/null +++ b/src/components/pages/reporting/reporting.page.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReportingDatesMolecule from "../../molecules/reporting-dates/reporting-dates.molecule"; + +export default function ReportingPage() { + return ( +
+ +
+ ); +} diff --git a/src/components/pages/routes/routes.page.jsx b/src/components/pages/routes/routes.page.jsx index cf05db8..4709e39 100644 --- a/src/components/pages/routes/routes.page.jsx +++ b/src/components/pages/routes/routes.page.jsx @@ -3,11 +3,13 @@ import React from "react"; import { connect } from "react-redux"; import { Route, Switch } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism"; -import Jobs from "../jobs/jobs.page"; -import SettingsPage from "../settings/settings.page"; import { selectBodyshop } from "../../../redux/user/user.selectors"; import ErrorResultAtom from "../../atoms/error-result/error-result.atom"; +import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism"; +import Jobs from "../jobs/jobs.page"; +import ReportingPage from "../reporting/reporting.page"; +import SettingsPage from "../settings/settings.page"; + const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({}); @@ -32,6 +34,7 @@ export function RoutesPage({ bodyshop }) { + diff --git a/src/components/pages/settings/settings.page.jsx b/src/components/pages/settings/settings.page.jsx index f23e018..8455ac5 100644 --- a/src/components/pages/settings/settings.page.jsx +++ b/src/components/pages/settings/settings.page.jsx @@ -28,11 +28,3 @@ export default function SettingsPage() { ); } - -// diff --git a/src/graphql/reporting.queries.js b/src/graphql/reporting.queries.js new file mode 100644 index 0000000..a7bbfb5 --- /dev/null +++ b/src/graphql/reporting.queries.js @@ -0,0 +1,47 @@ +import gql from "graphql-tag"; + +export const REPORTING_GET_JOBS = gql` + query REPORTING_GET_JOBS($startDate: date, $endDate: date) { + jobs( + where: { + _and: [ + { close_date: { _gte: $startDate } } + { close_date: { _lte: $endDate } } + { close_date: { _is_null: false } } + ] + } + ) { + ownr_ln + ownr_fn + ins_co_nm + group + clm_total + clm_no + close_date + id + loss_date + updated_at + v_age + v_makedesc + v_model + v_model_yr + joblines { + act_price + db_price + part_qty + part_type + price_diff + price_diff_pc + updated_at + oem_partno + line_no + line_ind + line_desc + ignore + id + db_ref + unq_seq + } + } + } +`; diff --git a/src/redux/application/application.sagas.js b/src/redux/application/application.sagas.js index 38fbe52..c69a008 100644 --- a/src/redux/application/application.sagas.js +++ b/src/redux/application/application.sagas.js @@ -1,4 +1,5 @@ import { all, call, takeLatest, select, put } from "redux-saga/effects"; +import GetJobTarget from "../../util/GetJobTarget"; import { setSelectedJobTargetPcSuccess } from "./application.actions"; import ApplicationActionTypes from "./application.types"; @@ -12,17 +13,18 @@ export function* CalculateTarget({ payload }) { const { group, v_age } = payload; const targets = yield select((state) => state.user.bodyshop.targets); - const targetsForGroup = targets.filter((t) => t.group === group); - if (!targetsForGroup) return 0; - const targetPc = targetsForGroup.filter( - (t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true) - ); - if (targetPc.length === 0) yield put(setSelectedJobTargetPcSuccess(100)); - else if (targetPc.length === 1) - yield put(setSelectedJobTargetPcSuccess(targetPc[0].target)); - else { - yield put(setSelectedJobTargetPcSuccess(100)); - } + yield put(setSelectedJobTargetPcSuccess(GetJobTarget(group, v_age, targets))); + // const targetsForGroup = targets.filter((t) => t.group === group); + // if (!targetsForGroup) return 0; + // const targetPc = targetsForGroup.filter( + // (t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true) + // ); + // if (targetPc.length === 0) yield put(setSelectedJobTargetPcSuccess(100)); + // else if (targetPc.length === 1) + // yield put(setSelectedJobTargetPcSuccess(targetPc[0].target)); + // else { + // yield put(setSelectedJobTargetPcSuccess(100)); + // } } export function* applicationSagas() { diff --git a/src/redux/reporting/reporting.actions.js b/src/redux/reporting/reporting.actions.js new file mode 100644 index 0000000..a7aa440 --- /dev/null +++ b/src/redux/reporting/reporting.actions.js @@ -0,0 +1,24 @@ +import ReportingActionTypes from "./reporting.types"; + +export const queryReportingData = ({ startDate, endDate }) => ({ + type: ReportingActionTypes.QUERY_REPORTING_DATA, + payload: { startDate, endDate }, +}); + +export const setReportingData = (data) => ({ + type: ReportingActionTypes.SET_REPORTING_DATA, + payload: data, +}); + +export const calculateScorecard = (data) => ({ + type: ReportingActionTypes.CALCULATE_SCORE_CARD, + payload: data, +}); +export const setScoreCard = (data) => ({ + type: ReportingActionTypes.SET_SCORE_CARD, + payload: data, +}); +export const setReportingError = (data) => ({ + type: ReportingActionTypes.SET_REPORTING_ERROR, + payload: data, +}); diff --git a/src/redux/reporting/reporting.reducer.js b/src/redux/reporting/reporting.reducer.js new file mode 100644 index 0000000..f77390c --- /dev/null +++ b/src/redux/reporting/reporting.reducer.js @@ -0,0 +1,23 @@ +import ReportingActionTypes from "./reporting.types"; +const INITIAL_STATE = { + dates: { startDate: null, endDate: null }, + data: [], + scoreCard: null, + error: null, + loading: false, +}; + +const applicationReducer = (state = INITIAL_STATE, action) => { + switch (action.type) { + case ReportingActionTypes.QUERY_REPORTING_DATA: + return { ...state, loading: true, dates: action.payload }; + case ReportingActionTypes.SET_REPORTING_DATA: + return { ...state, data: action.payload }; + case ReportingActionTypes.SET_SCORE_CARD: + return { ...state, loading: false, scoreCard: action.payload }; + default: + return state; + } +}; + +export default applicationReducer; diff --git a/src/redux/reporting/reporting.sagas.js b/src/redux/reporting/reporting.sagas.js new file mode 100644 index 0000000..5d02944 --- /dev/null +++ b/src/redux/reporting/reporting.sagas.js @@ -0,0 +1,57 @@ +import { all, call, takeLatest, select, put } from "redux-saga/effects"; +import { calculateScorecard, setReportingData } from "./reporting.actions"; +import ReportingApplicationTypes from "./reporting.types"; +import client from "../../graphql/GraphQLClient"; +import { REPORTING_GET_JOBS } from "../../graphql/reporting.queries"; +const { log } = window; + +export function* onQueryReportData() { + yield takeLatest( + ReportingApplicationTypes.QUERY_REPORTING_DATA, + queryReportingData + ); +} +export function* queryReportingData({ payload: { startDate, endDate } }) { + const result = yield client.query({ + query: REPORTING_GET_JOBS, + variables: { startDate, endDate }, + }); + if (result.errors) { + log.error("Error fetching report data.", result.errors); + yield put(setReportingData(null)); + } else { + yield put(setReportingData(result.data.jobs)); + } +} + +export function* onSetReportData() { + yield takeLatest( + ReportingApplicationTypes.SET_REPORTING_DATA, + handleSetReportData + ); +} +export function* handleSetReportData({ payload: jobs }) { + yield put(calculateScorecard(jobs)); +} + +export function* onCalculateScoreCard() { + yield takeLatest( + ReportingApplicationTypes.CALCULATE_SCORE_CARD, + handleCalculateScoreCard + ); +} +export function* handleCalculateScoreCard({ payload: jobs }) { + console.log("jobs", jobs); + // yield put(calculateScorecard(jobs)); + + //Get the RPS on a per job basis. + +} + +export function* reportingSagas() { + yield all([ + call(onQueryReportData), + call(onSetReportData), + call(onCalculateScoreCard), + ]); +} diff --git a/src/redux/reporting/reporting.selectors.js b/src/redux/reporting/reporting.selectors.js new file mode 100644 index 0000000..7780c2a --- /dev/null +++ b/src/redux/reporting/reporting.selectors.js @@ -0,0 +1,49 @@ +import { createSelector } from "reselect"; + +const selectReporting = (state) => state.reporting; + +export const selectReportLoading = createSelector( + [selectReporting], + (reporting) => reporting.loading +); +export const selectDates = createSelector( + [selectReporting], + (reporting) => reporting.dates +); +export const selectScorecard = createSelector( + [selectReporting], + (reporting) => reporting.scoreCard +); +export const selectReportingError = createSelector( + [selectReporting], + (reporting) => reporting.error +); +export const selectReportData = createSelector( + [selectReporting], + (reporting) => reporting.data +); + +// export const selectWatchedPaths = createSelector( +// [selectReporting], +// (application) => application.watchedPaths +// ); + +// export const selectWatcherError = createSelector( +// [selectReporting], +// (application) => application.watcherError +// ); + +// export const selectSelectedJobId = createSelector( +// [selectReporting], +// (application) => application.selectedJobId +// ); + +// export const selectSelectedJobTargetPc = createSelector( +// [selectReporting], +// (application) => application.selectedJobTargetPc +// ); + +// export const selectSettings = createSelector( +// [selectReporting], +// (application) => application.settings +// ); diff --git a/src/redux/reporting/reporting.types.js b/src/redux/reporting/reporting.types.js new file mode 100644 index 0000000..18c0755 --- /dev/null +++ b/src/redux/reporting/reporting.types.js @@ -0,0 +1,8 @@ +const ReportingActionTypes = { + QUERY_REPORTING_DATA: "QUERY_REPORTING_DATA", + CALCULATE_SCORE_CARD: "CALCULATE_SCORE_CARD", + SET_REPORTING_DATA: "SET_REPORTING_DATA", + SET_SCORE_CARD: "SET_SCORE_CARD", + SET_REPORTING_ERROR: "SET_REPORTING_ERROR", +}; +export default ReportingActionTypes; diff --git a/src/redux/root.reducer.js b/src/redux/root.reducer.js index 902b0fd..eaeda26 100644 --- a/src/redux/root.reducer.js +++ b/src/redux/root.reducer.js @@ -3,16 +3,18 @@ import { persistReducer } from "redux-persist"; 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"; const persistConfig = { key: "root", storage, - blacklist: ["application", "user"], + blacklist: ["application", "user", "reporting"], }; const rootReducer = combineReducers({ application: applicationReducer, user: userReducer, + reporting: reportingReducer, }); export default persistReducer(persistConfig, rootReducer); diff --git a/src/redux/root.saga.js b/src/redux/root.saga.js index 64c4946..d348a07 100644 --- a/src/redux/root.saga.js +++ b/src/redux/root.saga.js @@ -1,7 +1,7 @@ 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"; export default function* rootSaga() { - yield all([call(applicationSagas), call(userSagas)]); + yield all([call(applicationSagas), call(userSagas), call(reportingSagas)]); } diff --git a/src/util/CalculateJobRps.js b/src/util/CalculateJobRps.js new file mode 100644 index 0000000..f31aa09 --- /dev/null +++ b/src/util/CalculateJobRps.js @@ -0,0 +1,31 @@ +import Dinero from "dinero.js"; + +export function CalculateJobRpsDollars(job) { + if (!job) { + return 0; + } + return job.joblines + .filter((j) => !j.ignore) + .reduce((acc, val) => { + if (val.price_diff > 0) { + return acc.add( + Dinero({ amount: Math.round((val.price_diff || 0) * 100) }) + ); + } else { + return acc; + } + }, Dinero()); +} + +export function CalculateJobRpsPc(job, currentRpsDollars) { + //TODO Redo this to do total of db price - act price / db price + if (!job) { + return 0; + } + const dbPriceSum = job.joblines + .filter((j) => !j.ignore) + .reduce((acc, val) => { + return acc + val.db_price; + }, 0); + return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1); +} diff --git a/src/util/GetJobTarget.js b/src/util/GetJobTarget.js new file mode 100644 index 0000000..4919eaa --- /dev/null +++ b/src/util/GetJobTarget.js @@ -0,0 +1,12 @@ +export default function GetJobTarget(group, v_age, targets) { + const targetsForGroup = targets.filter((t) => t.group === group); + if (!targetsForGroup) return 0; + const targetPc = targetsForGroup.filter( + (t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true) + ); + if (targetPc.length === 0) return 100; + else if (targetPc.length === 1) return targetPc[0].target; + else { + return 100; + } +} From 329c9750198709a9d73ded2bdfd9a084fa645b20 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 20 Oct 2020 16:37:39 -0700 Subject: [PATCH 5/6] WIP report. Added stats, sagas for calculation, formatting. --- .../close-date-display.molecule.jsx | 4 +- .../jobs-search-fields.molecule.jsx | 2 +- .../jobs-targets-stats.molecule.jsx | 5 +- .../reporting-jobs-list.molecule.jsx | 140 ++++++++++++++++++ .../reporting-totals-stats.molecule.jsx | 73 +++++++++ .../pages/reporting/reporting.page.jsx | 21 ++- src/graphql/reporting.queries.js | 2 + src/redux/reporting/reporting.sagas.js | 81 +++++++++- src/util/CalculateJobRps.js | 23 ++- src/util/GetJobTarget.js | 4 +- 10 files changed, 335 insertions(+), 20 deletions(-) create mode 100644 src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx create mode 100644 src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx diff --git a/src/components/molecules/close-date-display/close-date-display.molecule.jsx b/src/components/molecules/close-date-display/close-date-display.molecule.jsx index d9a29ed..1f54701 100644 --- a/src/components/molecules/close-date-display/close-date-display.molecule.jsx +++ b/src/components/molecules/close-date-display/close-date-display.molecule.jsx @@ -29,7 +29,7 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) { return (
setEditMode(false)}> {loading && } @@ -38,7 +38,7 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) { return (
setEditMode(true)}> - {value.isValid() ? value.format("MM/DD/yyyy") : "No date set"} + {value && value.isValid() ? value.format("MM/DD/yyyy") : "No date set"}
); } diff --git a/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx b/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx index 45765d0..bd03303 100644 --- a/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx +++ b/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx @@ -7,7 +7,7 @@ export default function JobsSearchFieldsMolecule({ callSearchQuery }) { const handleFinish = (values) => { callSearchQuery({ variables: { - search: values.search, + search: values.search || "", startDate: (values.dateRange && values.dateRange[0]) || null, endDate: (values.dateRange && values.dateRange[1]) || null, }, diff --git a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx index 2c7008e..f7a7ccc 100644 --- a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx +++ b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx @@ -53,10 +53,9 @@ export function JobsTargetsStatsMolecule({ currentRpsPc ? "tomato" : "seagreen", + color: selectedJobTargetPc > currentRpsPc ? "tomato" : "seagreen", }} - value={currentRpsPc} + value={(currentRpsPc * 100).toFixed(1)} suffix="%" /> 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 new file mode 100644 index 0000000..829ed73 --- /dev/null +++ b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx @@ -0,0 +1,140 @@ +import { Input, Table } from "antd"; +import React, { useState } from "react"; +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"; +import PriceDiffPcFormatterAtom from "../../atoms/price-diff-pc-formatter/price-diff-pc-formatter.atom"; + +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + selectReportData, + selectReportLoading, +} from "../../../redux/reporting/reporting.selectors"; +const mapStateToProps = createStructuredSelector({ + reportingLoading: selectReportLoading, + reportData: selectReportData, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ReportingJobsListMolecule); + +export function ReportingJobsListMolecule({ reportingLoading, reportData }) { + 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 + }) - ${record.group} @ ${ + record.v_age === 1 ? `${record.v_age} year` : `${record.v_age} years` + }`, + }, + { + title: "Database Price Sum", + dataIndex: "dbPriceSum", + key: "dbPriceSum", + render: (text, record) => record.dbPriceSum.toFormat(), + }, + { + title: "Actual Price Sum ", + dataIndex: "actPriceSum", + key: "actPriceSum", + render: (text, record) => record.actPriceSum.toFormat(), + }, + { + title: "Price Diff.", + dataIndex: "jobRpsDollars", + key: "jobRpsDollars", + render: (text, record) => ( + record.jobTarget ? "seagreen" : "tomato", + }} + > + {`${record.jobRpsDollars.toFormat()} / ${record.expectedRpsDollars.toFormat()}`} + + ), + }, + { + title: "Price Diff. %", + dataIndex: "price_diff_pc", + key: "price_diff_pc", + render: (text, record) => ( + record.jobTarget ? "seagreen" : "tomato", + }} + > + {`${(record.jobRpsPc * 100).toFixed(1)}% / ${( + record.jobTarget * 100 + ).toFixed(1)}%`} + + ), + }, + ]; + + const data = + searchText !== "" + ? reportData.filter( + (j) => + j.ownr_fn.toLowerCase().includes(searchText.toLowerCase()) || + j.ownr_ln.toLowerCase().includes(searchText.toLowerCase()) || + j.ownr_clm_no.toLowerCase().includes(searchText.toLowerCase()) + ) + : reportData; + + return ( +
+
( + { + setSearchText(val); + }} + enterButton + allowClear + /> + )} + columns={columns} + rowKey="id" + loading={reportingLoading} + size="small" + pagination={false} + dataSource={data} + scroll={{ + x: true, + }} + /> + + ); +} diff --git a/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx new file mode 100644 index 0000000..63adb2c --- /dev/null +++ b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx @@ -0,0 +1,73 @@ +import { Skeleton, Statistic } from "antd"; +import React, { useCallback } from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectSelectedJobTargetPc } from "../../../redux/application/application.selectors"; +import { + selectReportLoading, + selectScorecard, +} from "../../../redux/reporting/reporting.selectors"; +import ErrorResultAtom from "../../atoms/error-result/error-result.atom"; + +const mapStateToProps = createStructuredSelector({ + reportingLoading: selectReportLoading, + scoreCard: selectScorecard, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(ReportingTotalsStatsMolecule); + +export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) { + if (reportingLoading) return ; + if (!scoreCard) + return ; + + return ( +
+ + + + + +
+ ); +} diff --git a/src/components/pages/reporting/reporting.page.jsx b/src/components/pages/reporting/reporting.page.jsx index 73a57d9..f6e6949 100644 --- a/src/components/pages/reporting/reporting.page.jsx +++ b/src/components/pages/reporting/reporting.page.jsx @@ -1,10 +1,29 @@ import React from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectDates } from "../../../redux/reporting/reporting.selectors"; import ReportingDatesMolecule from "../../molecules/reporting-dates/reporting-dates.molecule"; +import ReportingJobsListMolecule from "../../molecules/reporting-jobs-list/reporting-jobs-list.molecule"; +import ReportingTotalsStatsMolecule from "../../molecules/reporting-totals-stats/reporting-totals-stats.molecule"; -export default function ReportingPage() { +const mapStateToProps = createStructuredSelector({ + dates: selectDates, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(ReportingPage); + +export function ReportingPage({ dates }) { return (
+ {dates && dates.startDate && dates.endDate && ( +
+ + +
+ )}
); } diff --git a/src/graphql/reporting.queries.js b/src/graphql/reporting.queries.js index a7bbfb5..9625ca4 100644 --- a/src/graphql/reporting.queries.js +++ b/src/graphql/reporting.queries.js @@ -25,6 +25,8 @@ export const REPORTING_GET_JOBS = gql` v_makedesc v_model v_model_yr + v_vin + v_type joblines { act_price db_price diff --git a/src/redux/reporting/reporting.sagas.js b/src/redux/reporting/reporting.sagas.js index 5d02944..5624d96 100644 --- a/src/redux/reporting/reporting.sagas.js +++ b/src/redux/reporting/reporting.sagas.js @@ -1,8 +1,19 @@ import { all, call, takeLatest, select, put } from "redux-saga/effects"; -import { calculateScorecard, setReportingData } from "./reporting.actions"; +import { + calculateScorecard, + setReportingData, + setScoreCard, +} from "./reporting.actions"; import ReportingApplicationTypes from "./reporting.types"; import client from "../../graphql/GraphQLClient"; import { REPORTING_GET_JOBS } from "../../graphql/reporting.queries"; +import Dinero from "dinero.js"; +import { + CalculateJobRpsDollars, + CalculateJobRpsPc, +} from "../../util/CalculateJobRps"; +import GetJobTarget from "../../util/GetJobTarget"; + const { log } = window; export function* onQueryReportData() { @@ -20,7 +31,7 @@ export function* queryReportingData({ payload: { startDate, endDate } }) { log.error("Error fetching report data.", result.errors); yield put(setReportingData(null)); } else { - yield put(setReportingData(result.data.jobs)); + yield put(calculateScorecard(result.data.jobs)); } } @@ -31,7 +42,7 @@ export function* onSetReportData() { ); } export function* handleSetReportData({ payload: jobs }) { - yield put(calculateScorecard(jobs)); + // yield put(calculateScorecard(jobs)); } export function* onCalculateScoreCard() { @@ -42,10 +53,70 @@ export function* onCalculateScoreCard() { } export function* handleCalculateScoreCard({ payload: jobs }) { console.log("jobs", jobs); - // yield put(calculateScorecard(jobs)); + const targets = yield select((state) => state.user.bodyshop.targets); - //Get the RPS on a per job basis. + const scoreCard = { + shopRpsTotalDollars: Dinero(), + shopRpsExpectedDollars: Dinero(), + varianceDollars: null, + variancePc: 0, + allJobsSumDbPrice: Dinero(), + allJobsSumActPrice: Dinero(), + currentRpsPc: 0, + targetRpsPc: 0, + }; + //Get the RPS on a per job basis. + jobs = jobs.map((job) => { + const { actPriceSum, jobRpsDollars } = CalculateJobRpsDollars(job, true); + const { dbPriceSum, jobRpsPc } = CalculateJobRpsPc( + job, + jobRpsDollars, + true + ); + const jobTarget = GetJobTarget(job.group, job.v_age, targets); + scoreCard.shopRpsTotalDollars = scoreCard.shopRpsTotalDollars.add( + jobRpsDollars + ); + const expectedRpsDollars = dbPriceSum.percentage(jobTarget * 100); + scoreCard.shopRpsExpectedDollars = scoreCard.shopRpsExpectedDollars.add( + expectedRpsDollars + ); + + scoreCard.allJobsSumDbPrice = scoreCard.allJobsSumDbPrice.add(dbPriceSum); + scoreCard.allJobsSumActPrice = scoreCard.allJobsSumActPrice.add( + actPriceSum + ); + + //sum db price * percentage expected. + return { + ...job, + actPriceSum, + jobRpsDollars, + dbPriceSum, + jobRpsPc, + jobTarget, + expectedRpsDollars, + }; + }); + + scoreCard.varianceDollars = scoreCard.shopRpsTotalDollars.subtract( + scoreCard.shopRpsExpectedDollars + ); + + scoreCard.variancePc = + scoreCard.varianceDollars.getAmount() / + scoreCard.shopRpsExpectedDollars.getAmount(); + + scoreCard.currentRpsPc = + scoreCard.shopRpsTotalDollars.getAmount() / + scoreCard.allJobsSumDbPrice.getAmount(); + scoreCard.targetRpsPc = + scoreCard.shopRpsExpectedDollars.getAmount() / + scoreCard.allJobsSumDbPrice.getAmount(); + //Set the data. + yield put(setScoreCard(scoreCard)); + yield put(setReportingData(jobs)); } export function* reportingSagas() { diff --git a/src/util/CalculateJobRps.js b/src/util/CalculateJobRps.js index f31aa09..c97aeda 100644 --- a/src/util/CalculateJobRps.js +++ b/src/util/CalculateJobRps.js @@ -1,12 +1,16 @@ import Dinero from "dinero.js"; -export function CalculateJobRpsDollars(job) { +export function CalculateJobRpsDollars(job, returnSumActPrice) { if (!job) { return 0; } - return job.joblines + let actPriceSum = Dinero(); + const jobRpsDollars = job.joblines .filter((j) => !j.ignore) .reduce((acc, val) => { + actPriceSum = actPriceSum.add( + Dinero({ amount: Math.round((val.act_price || 0) * 100) }) + ); if (val.price_diff > 0) { return acc.add( Dinero({ amount: Math.round((val.price_diff || 0) * 100) }) @@ -15,9 +19,14 @@ export function CalculateJobRpsDollars(job) { return acc; } }, Dinero()); + return returnSumActPrice ? { actPriceSum, jobRpsDollars } : jobRpsDollars; } -export function CalculateJobRpsPc(job, currentRpsDollars) { +export function CalculateJobRpsPc( + job, + currentRpsDollars, + returnSumDbPrice = false +) { //TODO Redo this to do total of db price - act price / db price if (!job) { return 0; @@ -25,7 +34,9 @@ export function CalculateJobRpsPc(job, currentRpsDollars) { const dbPriceSum = job.joblines .filter((j) => !j.ignore) .reduce((acc, val) => { - return acc + val.db_price; - }, 0); - return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1); + return acc.add(Dinero({ amount: Math.round((val.db_price || 0) * 100) })); + }, Dinero()); + + const jobRpsPc = currentRpsDollars.getAmount() / dbPriceSum.getAmount(); + return returnSumDbPrice ? { dbPriceSum, jobRpsPc } : jobRpsPc; } diff --git a/src/util/GetJobTarget.js b/src/util/GetJobTarget.js index 4919eaa..df93f1a 100644 --- a/src/util/GetJobTarget.js +++ b/src/util/GetJobTarget.js @@ -4,9 +4,9 @@ export default function GetJobTarget(group, v_age, targets) { const targetPc = targetsForGroup.filter( (t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true) ); - if (targetPc.length === 0) return 100; + if (targetPc.length === 0) return 1; else if (targetPc.length === 1) return targetPc[0].target; else { - return 100; + return 1; } } From 013f0b091feea6a259f295e917fdbf8d141f8c04 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 20 Oct 2020 17:20:16 -0700 Subject: [PATCH 6/6] Basic reporting completed. --- electron/main.js | 10 +++---- .../reporting-title/reporting-title.atom.jsx | 22 ++++++++++++++ .../close-date-display.molecule.jsx | 3 +- .../jobs-detail-description.molecule.jsx | 4 +++ .../reporting-jobs-list.molecule.jsx | 29 ++++++++++++------- .../reporting-totals-stats.molecule.jsx | 3 +- src/components/pages/jobs/jobs.page.jsx | 11 ++----- .../pages/reporting/reporting.page.jsx | 2 ++ src/components/pages/routes/routes.page.jsx | 13 ++++----- src/graphql/jobs.queries.js | 1 + src/index.js | 6 ++-- src/redux/reporting/reporting.reducer.js | 9 +++++- src/util/constants.js | 1 + 13 files changed, 76 insertions(+), 38 deletions(-) create mode 100644 src/components/atoms/reporting-title/reporting-title.atom.jsx create mode 100644 src/util/constants.js diff --git a/electron/main.js b/electron/main.js index 06e3178..1b8fd64 100644 --- a/electron/main.js +++ b/electron/main.js @@ -23,11 +23,11 @@ log.info("App starting..."); // Conditionally include the dev tools installer to load React Dev Tools let installExtension, REACT_DEVELOPER_TOOLS; -// if (isDev) { -// const devTools = require("electron-devtools-installer"); -// installExtension = devTools.default; -// REACT_DEVELOPER_TOOLS = devTools.REACT_DEVELOPER_TOOLS; -// } +if (isDev) { + const devTools = require("electron-devtools-installer"); + installExtension = devTools.default; + REACT_DEVELOPER_TOOLS = devTools.REACT_DEVELOPER_TOOLS; +} var menu = Menu.buildFromTemplate([ { diff --git a/src/components/atoms/reporting-title/reporting-title.atom.jsx b/src/components/atoms/reporting-title/reporting-title.atom.jsx new file mode 100644 index 0000000..7f9cfce --- /dev/null +++ b/src/components/atoms/reporting-title/reporting-title.atom.jsx @@ -0,0 +1,22 @@ +import { Typography } from "antd"; +import React from "react"; + +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectDates } from "../../../redux/reporting/reporting.selectors"; +import moment from "moment"; +import { DateFormat } from "../../../util/constants"; +const mapStateToProps = createStructuredSelector({ + dates: selectDates, +}); + +export function ReportingTitleAtom({ dates }) { + return ( + + {`RPS Report for Period from ${moment(dates.startDate).format( + DateFormat + )} to ${moment(dates.endDate).format(DateFormat)}`} + + ); +} +export default connect(mapStateToProps, null)(ReportingTitleAtom); diff --git a/src/components/molecules/close-date-display/close-date-display.molecule.jsx b/src/components/molecules/close-date-display/close-date-display.molecule.jsx index 1f54701..df83547 100644 --- a/src/components/molecules/close-date-display/close-date-display.molecule.jsx +++ b/src/components/molecules/close-date-display/close-date-display.molecule.jsx @@ -3,6 +3,7 @@ import { DatePicker, message, Spin } from "antd"; import moment from "moment"; import React, { useState } from "react"; import { UPDATE_JOB } from "../../../graphql/jobs.queries"; +import { DateFormat } from "../../../util/constants"; export default function CloseDateDisplayMolecule({ jobId, close_date }) { const [editMode, setEditMode] = useState(false); @@ -38,7 +39,7 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) { return (
setEditMode(true)}> - {value && value.isValid() ? value.format("MM/DD/yyyy") : "No date set"} + {value && value.isValid() ? value.format(DateFormat) : "No date set"}
); } diff --git a/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx b/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx index 911584d..fc9e4cb 100644 --- a/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx +++ b/src/components/molecules/jobs-detail-description/jobs-detail-description.molecule.jsx @@ -3,6 +3,7 @@ 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"; export default function JobsDetailDescriptionMolecule({ loading, job }) { if (loading) return ; @@ -26,6 +27,9 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) { close_date={job.close_date} /> + + {job.updated_at} + 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 829ed73..9e3fa8c 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,29 +1,26 @@ import { Input, Table } from "antd"; import React, { useState } from "react"; -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"; -import PriceDiffPcFormatterAtom from "../../atoms/price-diff-pc-formatter/price-diff-pc-formatter.atom"; - import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; 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, }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + setSelectedJobId: (id) => dispatch(setSelectedJobId(id)), }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(ReportingJobsListMolecule); -export function ReportingJobsListMolecule({ reportingLoading, reportData }) { +export function ReportingJobsListMolecule({ + reportingLoading, + reportData, + setSelectedJobId, +}) { const [searchText, setSearchText] = useState(""); const columns = [ @@ -31,6 +28,11 @@ export function ReportingJobsListMolecule({ reportingLoading, reportData }) { title: "Claim No.", dataIndex: "clm_no", key: "clm_no", + render: (text, record) => ( + setSelectedJobId(record.id)} to={"/"}> + {text} + + ), }, { title: "Ins Co.", @@ -138,3 +140,8 @@ export function ReportingJobsListMolecule({ reportingLoading, reportData }) { ); } + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ReportingJobsListMolecule); diff --git a/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx index 63adb2c..c17b519 100644 --- a/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx +++ b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx @@ -1,8 +1,7 @@ import { Skeleton, Statistic } from "antd"; -import React, { useCallback } from "react"; +import React from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import { selectSelectedJobTargetPc } from "../../../redux/application/application.selectors"; import { selectReportLoading, selectScorecard, diff --git a/src/components/pages/jobs/jobs.page.jsx b/src/components/pages/jobs/jobs.page.jsx index 160ae86..e6eef89 100644 --- a/src/components/pages/jobs/jobs.page.jsx +++ b/src/components/pages/jobs/jobs.page.jsx @@ -1,15 +1,11 @@ -import { Col, Row, Tabs, Grid } from "antd"; +import { Col, Grid, Row, Tabs } from "antd"; import React from "react"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; import JobsDetailOrganism from "../../organisms/jobs-detail/jobs-detail.organism"; import JobsListOrganism from "../../organisms/jobs-list-latest/jobs-list-latest.organism"; import JobsListSearchOrganism from "../../organisms/jobs-list-search/jobs-list-search.organism"; -const mapStateToProps = createStructuredSelector({}); -const mapDispatchToProps = (dispatch) => ({}); - -export function JobsPage() { +export default function JobsPage() { + console.log("Jobs Page Rerender"); const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) .filter((screen) => !!screen[1]) .slice(-1)[0]; @@ -47,4 +43,3 @@ export function JobsPage() { ); } -export default connect(mapStateToProps, mapDispatchToProps)(JobsPage); diff --git a/src/components/pages/reporting/reporting.page.jsx b/src/components/pages/reporting/reporting.page.jsx index f6e6949..6b78822 100644 --- a/src/components/pages/reporting/reporting.page.jsx +++ b/src/components/pages/reporting/reporting.page.jsx @@ -2,6 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectDates } from "../../../redux/reporting/reporting.selectors"; +import ReportingTitleAtom from "../../atoms/reporting-title/reporting-title.atom"; import ReportingDatesMolecule from "../../molecules/reporting-dates/reporting-dates.molecule"; import ReportingJobsListMolecule from "../../molecules/reporting-jobs-list/reporting-jobs-list.molecule"; import ReportingTotalsStatsMolecule from "../../molecules/reporting-totals-stats/reporting-totals-stats.molecule"; @@ -20,6 +21,7 @@ export function ReportingPage({ dates }) { {dates && dates.startDate && dates.endDate && (
+
diff --git a/src/components/pages/routes/routes.page.jsx b/src/components/pages/routes/routes.page.jsx index 4709e39..1f523b6 100644 --- a/src/components/pages/routes/routes.page.jsx +++ b/src/components/pages/routes/routes.page.jsx @@ -1,12 +1,12 @@ import { Layout } from "antd"; import React from "react"; import { connect } from "react-redux"; -import { Route, Switch } from "react-router-dom"; +import { Route } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../../redux/user/user.selectors"; import ErrorResultAtom from "../../atoms/error-result/error-result.atom"; import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism"; -import Jobs from "../jobs/jobs.page"; +import JobsPage from "../jobs/jobs.page"; import ReportingPage from "../reporting/reporting.page"; import SettingsPage from "../settings/settings.page"; @@ -21,6 +21,7 @@ export function RoutesPage({ bodyshop }) { errorMessage="You do not currently have access to any shop. Please reach out to technical support." /> ); + console.log("routes render"); return ( - - - - - + + + diff --git a/src/graphql/jobs.queries.js b/src/graphql/jobs.queries.js index 73be1ad..fb87aa6 100644 --- a/src/graphql/jobs.queries.js +++ b/src/graphql/jobs.queries.js @@ -89,6 +89,7 @@ export const QUERY_JOB_BY_PK = gql` v_age loss_date close_date + updated_at joblines(order_by: { line_no: asc }) { id line_no diff --git a/src/index.js b/src/index.js index 2d5ab9d..3e272e9 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import "antd/dist/antd.css"; import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; -import { BrowserRouter } from "react-router-dom"; +import { MemoryRouter } from "react-router-dom"; import { PersistGate } from "redux-persist/integration/react"; import App from "./App/App"; import "./index.css"; @@ -11,11 +11,11 @@ require("dotenv").config(); ReactDOM.render( - + - + , document.getElementById("root") ); diff --git a/src/redux/reporting/reporting.reducer.js b/src/redux/reporting/reporting.reducer.js index f77390c..64076cb 100644 --- a/src/redux/reporting/reporting.reducer.js +++ b/src/redux/reporting/reporting.reducer.js @@ -10,7 +10,14 @@ const INITIAL_STATE = { const applicationReducer = (state = INITIAL_STATE, action) => { switch (action.type) { case ReportingActionTypes.QUERY_REPORTING_DATA: - return { ...state, loading: true, dates: action.payload }; + return { + ...state, + loading: true, + dates: { + startDate: action.payload.startDate.toISOString(), + endDate: action.payload.endDate.toISOString(), + }, + }; case ReportingActionTypes.SET_REPORTING_DATA: return { ...state, data: action.payload }; case ReportingActionTypes.SET_SCORE_CARD: diff --git a/src/util/constants.js b/src/util/constants.js new file mode 100644 index 0000000..f8d19ea --- /dev/null +++ b/src/util/constants.js @@ -0,0 +1 @@ +export const DateFormat = "MM/DD/yyyy";