From ad7cbb308b59a166e48a4b95800de3d428906f42 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 22 Oct 2020 12:38:33 -0700 Subject: [PATCH] Added log rocket + analytics to ensure functionality --- electron/analytics.js | 32 ++++ electron/decoder/decoder.js | 2 + electron/file-scan/file-scan-ipc.js | 2 + electron/file-scan/file-scan.js | 3 + electron/file-watcher/file-watcher.js | 27 +-- electron/main.js | 5 +- electron/preload.js | 3 +- package-lock.json | 156 +++++++++++++++++- package.json | 2 + .../atoms/delete-job/delete-job.atom.jsx | 6 + .../ignore-job-line/ignore-job-line.atom.jsx | 11 +- .../job-group/job-group.molecule.jsx | 11 +- .../jobs-detail-description.molecule.jsx | 2 +- .../jobs-lines-table.molecule.jsx | 13 +- .../jobs-search-fields.molecule.jsx | 7 + .../reporting-dates.molecule.jsx | 1 - .../shop-settings-form.molecule.jsx | 1 - .../shop-settings/shop-settings.organism.jsx | 15 +- src/components/pages/sign-in/sign-in.page.jsx | 43 ++++- src/index.js | 2 + src/ipc.types.js | 2 + src/ipc/ipc-estimate-utils.js | 8 +- src/redux/application/application.sagas.js | 2 +- src/redux/reporting/reporting.sagas.js | 151 +++++++++-------- src/redux/user/user.reducer.js | 2 +- src/redux/user/user.sagas.js | 59 ++++--- 26 files changed, 434 insertions(+), 134 deletions(-) create mode 100644 electron/analytics.js diff --git a/electron/analytics.js b/electron/analytics.js new file mode 100644 index 0000000..dfb1bda --- /dev/null +++ b/electron/analytics.js @@ -0,0 +1,32 @@ +const { ipcMain } = require("electron"); +const { app } = require("electron"); +const log = require("electron-log"); +const Nucleus = require("nucleus-nodejs"); +const { default: ipcTypes } = require("../src/ipc.types"); + +Nucleus.init("5f91b569b95bac34eefdb63a", { debug: true }); + +Nucleus.setProps({ + version: app.getVersion().toString(), +}); + +Nucleus.onError = (type, err) => { + log.error(err); + // type will either be uncaughtException, unhandledRejection or windowError +}; + +ipcMain.on(ipcTypes.app.toMain.setUserName, (event, userName) => { + Nucleus.appStarted(); + Nucleus.setUserId(userName); +}); + +ipcMain.on(ipcTypes.app.toMain.track, (e, args) => { + console.log("args", args); + log.log("Received Tracking Request", args); + const { event, ...eventDetails } = args; + try { + Nucleus.track(event, eventDetails); + } catch (error) { + log.error(error); + } +}); diff --git a/electron/decoder/decoder.js b/electron/decoder/decoder.js index 3182cc3..e49e828 100644 --- a/electron/decoder/decoder.js +++ b/electron/decoder/decoder.js @@ -8,6 +8,7 @@ const ipcTypes = require("../../src/ipc.types"); const { NewNotification, } = require("../notification-wrapper/notification-wrapper"); +const Nucleus = require("nucleus-nodejs"); async function ImportJob(path) { const b = BrowserWindow.getAllWindows()[0]; @@ -26,6 +27,7 @@ async function ImportJob(path) { }); } else { log.info(`Ignored job. ${newJob.ERROR}`); + Nucleus.track("IGNORE_JOB", { reason: newJob.ERROR }); NewNotification({ title: "Job Ignored", body: newJob.ERROR, diff --git a/electron/file-scan/file-scan-ipc.js b/electron/file-scan/file-scan-ipc.js index 4f5d84a..10a4568 100644 --- a/electron/file-scan/file-scan-ipc.js +++ b/electron/file-scan/file-scan-ipc.js @@ -1,4 +1,5 @@ const { ipcMain } = require("electron"); +const Nucleus = require("nucleus-nodejs"); const ipcTypes = require("../../src/ipc.types"); const { ImportJob } = require("../decoder/decoder"); const { GetListOfEstimates } = require("./file-scan"); @@ -17,6 +18,7 @@ ipcMain.on( ipcMain.on( ipcTypes.default.fileScan.toMain.importJob, async (event, filePath) => { + Nucleus.track("IMPORT_JOB_FROM_SCAN"); await ImportJob(filePath); } ); diff --git a/electron/file-scan/file-scan.js b/electron/file-scan/file-scan.js index 01e65e1..17cf699 100644 --- a/electron/file-scan/file-scan.js +++ b/electron/file-scan/file-scan.js @@ -5,8 +5,11 @@ const log = require("electron-log"); const fsPromises = fs.promises; const _ = require("lodash"); const { DecodeEstimate } = require("../decoder/decoder"); +const Nucleus = require("nucleus-nodejs"); async function GetListOfEstimates() { + Nucleus.track("SCAN_ALL_ESTIMATES"); + log.info("Scanning all local estimates.."); const ListOfEnvFiles = await GetEnvFiles(); const ListOfSummarizedEstimates = await ReadAllEstimates(ListOfEnvFiles); const FilteredListOfSummarizedEstimates = ListOfSummarizedEstimates.filter( diff --git a/electron/file-watcher/file-watcher.js b/electron/file-watcher/file-watcher.js index ca2dd5c..4b3d96b 100644 --- a/electron/file-watcher/file-watcher.js +++ b/electron/file-watcher/file-watcher.js @@ -8,6 +8,7 @@ const { NewNotification, } = require("../notification-wrapper/notification-wrapper"); const log = require("electron-log"); +const Nucleus = require("nucleus-nodejs"); var watcher; async function StartWatcher() { @@ -53,23 +54,24 @@ async function StartWatcher() { console.log("File", path, "has been added"); HandleNewFile(path); }) - .on("addDir", function (path) { - console.log("Directory", path, "has been added"); - }) + // .on("addDir", function (path) { + // console.log("Directory", path, "has been added"); + // }) .on("change", async function (path) { console.log("File", path, "has been changed"); HandleNewFile(path); }) - .on("unlink", function (path) { - console.log("File", path, "has been removed"); - }) - .on("unlinkDir", function (path) { - console.log("Directory", path, "has been removed"); - }) + // .on("unlink", function (path) { + // console.log("File", path, "has been removed"); + // }) + // .on("unlinkDir", function (path) { + // console.log("Directory", path, "has been removed"); + // }) .on("error", function (error) { - console.log("Error happened", error); + log.error("Error in Watcher", error); const b = BrowserWindow.getFocusedWindow(); b.webContents.send(ipcTypes.fileWatcher.toRenderer.error, error); + Nucleus.track("WATCHER_ERROR", error); }) .on("ready", onWatcherReady) .on("raw", function (event, path, details) { @@ -81,14 +83,14 @@ async function StartWatcher() { } function onWatcherReady() { - console.log("Ready!"); + log.info("Watcher ready!"); const b = BrowserWindow.getAllWindows()[0]; b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.startSuccess); NewNotification({ title: "RPS Watcher Started", body: "Newly exported estimates will be automatically uploaded.", }); - console.log("Confirmed watched paths:", watcher.getWatched()); + log.info("Confirmed watched paths:", watcher.getWatched()); } async function StopWatcher() { @@ -107,5 +109,6 @@ exports.StopWatcher = StopWatcher; exports.watcher = watcher; async function HandleNewFile(path) { + Nucleus.track("IMPORT_JOB_FROM_WATCHER"); await ImportJob(path); } diff --git a/electron/main.js b/electron/main.js index 048aa9d..f8fbbd7 100644 --- a/electron/main.js +++ b/electron/main.js @@ -15,11 +15,13 @@ const { store } = require("./electron-store"); const { autoUpdater } = require("electron-updater"); const log = require("electron-log"); const { default: logger } = require("redux-logger"); +const Nucleus = require("nucleus-nodejs"); require("./ipc-main-handler"); +require("./analytics"); autoUpdater.logger = log; autoUpdater.logger.transports.file.level = "info"; -log.info("App starting..."); +log.info("App starting...", app.getVersion()); // Conditionally include the dev tools installer to load React Dev Tools let installExtension, REACT_DEVELOPER_TOOLS; @@ -247,6 +249,7 @@ autoUpdater.on("update-downloaded", (ev, info) => { console.log("Update downloaded; will install in 5 seconds"); }); autoUpdater.on("update-downloaded", (ev, info) => { + Nucleus.track("UPDATE_DOWNLOADED", info); if (process.env.NODE_ENV === "production") { dialog.showMessageBox( { diff --git a/electron/preload.js b/electron/preload.js index 8b7886e..77becb1 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -3,6 +3,7 @@ const { contextBridge, ipcRenderer } = require("electron"); const log = require("electron-log"); //ipcRenderer.removeAllListeners(); + contextBridge.exposeInMainWorld("logger", { info: (...msg) => { log.info(...msg); @@ -26,7 +27,7 @@ contextBridge.exposeInMainWorld("ipcRenderer", { // whitelist channels // let validChannels = ["toMain"]; // if (validChannels.includes(channel)) { - console.log("ipcRenderer Send", channel); + log.info("[Main] ipcRenderer Send", channel); ipcRenderer.send(channel, data); //} }, diff --git a/package-lock.json b/package-lock.json index 1e839b6..18b148e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "imexrps", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3318,6 +3318,11 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "arch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.2.tgz", + "integrity": "sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==" + }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", @@ -9582,6 +9587,12 @@ "loose-envify": "^1.0.0" } }, + "invert-kv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-3.0.1.tgz", + "integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==", + "optional": true + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -11902,6 +11913,15 @@ "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.4.tgz", "integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q==" }, + "lcid": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-3.1.1.tgz", + "integrity": "sha512-M6T051+5QCGLBQb8id3hdvIW8+zeFV2FyBGFS9IEK5H9Wt4MueD4bW1eWikpHgZp+5xR3l5c8pZUkQsIA0BFZg==", + "optional": true, + "requires": { + "invert-kv": "^3.0.0" + } + }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -12121,6 +12141,11 @@ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==" }, + "logrocket": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/logrocket/-/logrocket-1.0.14.tgz", + "integrity": "sha512-notwwiIiXOmWSKQDsW8UrFJPu81u9rd6YaIFBmx6uF0XtXXwNQ+Mvteh5WHdABWcQ2nN4I7QkQrCAocYDx7OVg==" + }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -12193,6 +12218,15 @@ "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==" }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "optional": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -12246,6 +12280,25 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "mem": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-5.1.1.tgz", + "integrity": "sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==", + "optional": true, + "requires": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^2.1.0", + "p-is-promise": "^2.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "optional": true + } + } + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -12878,6 +12931,12 @@ } } }, + "node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "optional": true + }, "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", @@ -13047,6 +13106,77 @@ "boolbase": "~1.0.0" } }, + "nucleus-nodejs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nucleus-nodejs/-/nucleus-nodejs-3.0.6.tgz", + "integrity": "sha512-IpgQqBlU9QZebPAfQadSODQmoHPsSABAlkTNFtuO8tKv+072NQKeKN+1CUxY8Xe9WpyOhLjFuBGQE3TpC81Dcg==", + "requires": { + "arch": "^2.1.1", + "conf": "^6.1.0", + "node-machine-id": "^1.1.12", + "os-locale": "^4.0.0", + "ws": "^7.1.2" + }, + "dependencies": { + "conf": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/conf/-/conf-6.2.4.tgz", + "integrity": "sha512-GjgyPRLo1qK1LR9RWAdUagqo+DP18f5HWCFk4va7GS+wpxQTOzfuKTwKOvGW2c01/YXNicAyyoyuSddmdkBzZQ==", + "optional": true, + "requires": { + "ajv": "^6.10.2", + "debounce-fn": "^3.0.1", + "dot-prop": "^5.0.0", + "env-paths": "^2.2.0", + "json-schema-typed": "^7.0.1", + "make-dir": "^3.0.0", + "onetime": "^5.1.0", + "pkg-up": "^3.0.1", + "semver": "^6.2.0", + "write-file-atomic": "^3.0.0" + } + }, + "debounce-fn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-3.0.1.tgz", + "integrity": "sha512-aBoJh5AhpqlRoHZjHmOzZlRx+wz2xVwGL9rjs+Kj0EWUrL4/h4K7OD176thl2Tdoqui/AaA4xhHrNArGLAaI3Q==", + "optional": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "optional": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "optional": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "optional": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "optional": true + } + } + }, "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", @@ -13387,6 +13517,17 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, + "os-locale": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-4.0.0.tgz", + "integrity": "sha512-HsSR1+2l6as4Wp2SGZxqLnuFHxVvh1Ir9pvZxyujsC13egZVe7P0YeBLN0ijQzM/twrO5To3ia3jzBXAvpMTEA==", + "optional": true, + "requires": { + "execa": "^1.0.0", + "lcid": "^3.0.0", + "mem": "^5.0.0" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -13407,6 +13548,12 @@ "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "dev": true }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "optional": true + }, "p-each-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", @@ -13420,6 +13567,12 @@ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "optional": true + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -18691,7 +18844,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, "requires": { "is-typedarray": "^1.0.0" } diff --git a/package.json b/package.json index 71072d3..64b234a 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,11 @@ "firebase": "^7.24.0", "graphql": "^15.3.0", "lodash": "^4.17.20", + "logrocket": "^1.0.14", "moment": "^2.29.1", "node-notifier": "^8.0.0", "node-sass": "^4.14.1", + "nucleus-nodejs": "^3.0.6", "query-string": "^6.13.6", "react": "^16.14.0", "react-dom": "^16.14.0", diff --git a/src/components/atoms/delete-job/delete-job.atom.jsx b/src/components/atoms/delete-job/delete-job.atom.jsx index c1407a9..2f2ed26 100644 --- a/src/components/atoms/delete-job/delete-job.atom.jsx +++ b/src/components/atoms/delete-job/delete-job.atom.jsx @@ -5,18 +5,24 @@ import React, { useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { DELETE_JOB } from "../../../graphql/jobs.queries"; +import ipcTypes from "../../../ipc.types"; import { setSelectedJobId } from "../../../redux/application/application.actions"; +const { ipcRenderer } = window; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ setSelectedJobId: (id) => dispatch(setSelectedJobId(id)), }); + export function DeleteJobAtom({ setSelectedJobId, jobId }) { const [deleteJob] = useMutation(DELETE_JOB); const [loading, setLoading] = useState(false); const handleDelete = async () => { setLoading(true); + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "DELETE_JOB", + }); const result = await deleteJob({ variables: { jobId: jobId }, }); 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 index b9e2cdc..2f9f9db 100644 --- a/src/components/atoms/ignore-job-line/ignore-job-line.atom.jsx +++ b/src/components/atoms/ignore-job-line/ignore-job-line.atom.jsx @@ -2,13 +2,20 @@ 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; +import ipcTypes from "../../../ipc.types"; +const { log, ipcRenderer } = window; -export default function IgnoreJobLineAtom({ ignore, lineId }) { +export default function IgnoreJobLineAtom({ ignore, lineId, line_desc }) { const [updateJobLine] = useMutation(UPDATE_JOB_LINE); const [loading, setLoading] = useState(false); const handleChange = async (checked) => { setLoading(true); + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "TOGGLE_IGNORE_LINE", + line_desc: line_desc, + ignore: checked, + }); + const result = await updateJobLine({ variables: { lineId: lineId, line: { ignore: checked } }, }); diff --git a/src/components/molecules/job-group/job-group.molecule.jsx b/src/components/molecules/job-group/job-group.molecule.jsx index 09186e1..b970c5c 100644 --- a/src/components/molecules/job-group/job-group.molecule.jsx +++ b/src/components/molecules/job-group/job-group.molecule.jsx @@ -5,7 +5,10 @@ import React, { useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { UPDATE_JOB } from "../../../graphql/jobs.queries"; +import ipcTypes from "../../../ipc.types"; import { selectBodyshop } from "../../../redux/user/user.selectors"; +const { ipcRenderer } = window; + const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser bodyshop: selectBodyshop, @@ -15,11 +18,17 @@ const mapDispatchToProps = (dispatch) => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(JobGroupMolecule); -export function JobGroupMolecule({ bodyshop, jobId, group }) { +export function JobGroupMolecule({ bodyshop, jobId, group, job }) { const [loading, setLoading] = useState(false); const [updateJob] = useMutation(UPDATE_JOB); const handleMenuClick = async (value) => { + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "CHANGE_VEHICLE_GROUP", + vehicle: `${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type})`, + oldGroup: group, + newGroup: value.key, + }); setLoading(true); const result = await updateJob({ variables: { jobId: jobId, job: { group: value.key } }, 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 1c832d1..0076de5 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 @@ -21,7 +21,7 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) { {job.clm_total} - + {job.v_age} 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 7cb39c7..49ffc20 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,11 +1,14 @@ import { Input, Table } from "antd"; import React, { useState } from "react"; +import ipcTypes from "../../../ipc.types"; import { alphaSort } from "../../../util/sorters"; import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom"; import IgnoreJobLine from "../../atoms/ignore-job-line/ignore-job-line.atom"; import partTypeConverterAtom from "../../atoms/part-type-converter/part-type-converter.atom"; import PriceDiffPcFormatterAtom from "../../atoms/price-diff-pc-formatter/price-diff-pc-formatter.atom"; +const { ipcRenderer } = window; + export default function JobLinesTableMolecule({ loading, job }) { const [searchText, setSearchText] = useState(""); @@ -92,7 +95,11 @@ export default function JobLinesTableMolecule({ loading, job }) { ], onFilter: (value, record) => value === record.ignore, render: (text, record) => ( - + ), }, ]; @@ -111,6 +118,10 @@ export default function JobLinesTableMolecule({ loading, job }) { { + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "JOB_LINES_SEARCH", + query: val, + }); setSearchText(val); }} enterButton 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 bd03303..5e869da 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 @@ -1,10 +1,17 @@ import { SearchOutlined } from "@ant-design/icons"; import { Button, DatePicker, Form, Input } from "antd"; import React from "react"; +import ipcTypes from "../../../ipc.types"; +const { ipcRenderer } = window; export default function JobsSearchFieldsMolecule({ callSearchQuery }) { const [form] = Form.useForm(); const handleFinish = (values) => { + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "SEARCH_JOBS", + query: values.search, + datesIncluded: !!values.dateRange, + }); callSearchQuery({ variables: { search: values.search || "", diff --git a/src/components/molecules/reporting-dates/reporting-dates.molecule.jsx b/src/components/molecules/reporting-dates/reporting-dates.molecule.jsx index b908a9a..00f054a 100644 --- a/src/components/molecules/reporting-dates/reporting-dates.molecule.jsx +++ b/src/components/molecules/reporting-dates/reporting-dates.molecule.jsx @@ -18,7 +18,6 @@ export function ReportingDatesMolecule({ queryReportingData }) { const [form] = Form.useForm(); const handleFinish = (values) => { - console.log("values", values); queryReportingData({ startDate: values.dateRange[0], endDate: values.dateRange[1], diff --git a/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx b/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx index 0ea61d3..38584c6 100644 --- a/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx +++ b/src/components/molecules/shop-settings-form/shop-settings-form.molecule.jsx @@ -9,7 +9,6 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) { form.getFieldValue("groups") || [] ); const handleBlur = () => { - console.log(form.getFieldValue("groups") || []); setGroupOptions(form.getFieldValue("groups") || []); }; diff --git a/src/components/organisms/shop-settings/shop-settings.organism.jsx b/src/components/organisms/shop-settings/shop-settings.organism.jsx index 981ef37..059fbd7 100644 --- a/src/components/organisms/shop-settings/shop-settings.organism.jsx +++ b/src/components/organisms/shop-settings/shop-settings.organism.jsx @@ -4,10 +4,11 @@ import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../../graphql/bodyshop.queries"; +import ipcTypes from "../../../ipc.types"; import { setBodyshop } from "../../../redux/user/user.actions"; import ErrorResultAtom from "../../atoms/error-result/error-result.atom"; import ShopSettingsFormMolecule from "../../molecules/shop-settings-form/shop-settings-form.molecule"; - +const { ipcRenderer } = window; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser }); @@ -15,10 +16,6 @@ const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) setBodyshop: (shop) => dispatch(setBodyshop(shop)), }); -export default connect( - mapStateToProps, - mapDispatchToProps -)(ShopSettingsOrganism); export function ShopSettingsOrganism({ setBodyshop }) { const { loading, error, data } = useQuery(QUERY_BODYSHOP); @@ -32,6 +29,9 @@ export function ShopSettingsOrganism({ setBodyshop }) { const handleFinish = async (values) => { setSaveLoading(true); + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "UPDATE_SHOP_DETAILS", + }); const result = await updateBodyshop({ variables: { id: data.bodyshops[0].id, shop: values }, @@ -70,3 +70,8 @@ export function ShopSettingsOrganism({ setBodyshop }) { ); } + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ShopSettingsOrganism); diff --git a/src/components/pages/sign-in/sign-in.page.jsx b/src/components/pages/sign-in/sign-in.page.jsx index 26087bc..b90ed8a 100644 --- a/src/components/pages/sign-in/sign-in.page.jsx +++ b/src/components/pages/sign-in/sign-in.page.jsx @@ -5,8 +5,10 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import ImEXOnlineLogo from "../../../assets/logo192.png"; import { emailSignInStart } from "../../../redux/user/user.actions"; +import { sendPasswordReset } from "../../../redux/user/user.actions"; import { selectLoginLoading, + selectPasswordReset, selectSignInError, } from "../../../redux/user/user.selectors"; import "./sign-in.page.styles.scss"; @@ -14,20 +16,33 @@ import "./sign-in.page.styles.scss"; const mapStateToProps = createStructuredSelector({ signInError: selectSignInError, loginLoading: selectLoginLoading, + passwordReset: selectPasswordReset, }); const mapDispatchToProps = (dispatch) => ({ emailSignInStart: (email, password) => dispatch(emailSignInStart({ email, password })), + sendPasswordReset: (email) => dispatch(sendPasswordReset(email)), }); -export function SignInPage({ emailSignInStart, signInError, loginLoading }) { +export function SignInPage({ + emailSignInStart, + signInError, + loginLoading, + sendPasswordReset, + passwordReset, +}) { const handleFinish = (values) => { const { email, password } = values; emailSignInStart(email, password); }; const [form] = Form.useForm(); + const handleReset = () => { + const email = form.getFieldValue("email"); + sendPasswordReset(email); + }; + return (
@@ -35,10 +50,16 @@ export function SignInPage({ emailSignInStart, signInError, loginLoading }) { ImEX RPS
- + } placeholder="Email" /> - + } type="password" @@ -46,7 +67,7 @@ export function SignInPage({ emailSignInStart, signInError, loginLoading }) { /> {signInError ? ( - + ) : null} + + {() => { + return ( + + ); + }} + + {passwordReset.error &&
{passwordReset.error}
}
); } diff --git a/src/index.js b/src/index.js index 3e272e9..e6db681 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ import "antd/dist/antd.css"; +import LogRocket from "logrocket"; import React from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; @@ -8,6 +9,7 @@ import App from "./App/App"; import "./index.css"; import { persistor, store } from "./redux/store"; require("dotenv").config(); +LogRocket.init("imex/rps"); ReactDOM.render( diff --git a/src/ipc.types.js b/src/ipc.types.js index 6ee0fa0..94d639f 100644 --- a/src/ipc.types.js +++ b/src/ipc.types.js @@ -7,6 +7,8 @@ exports.default = { app: { toMain: { setAcceptableInsCoNm: "setAcceptableInsCoNm", + setUserName: "setUserName", + track: "analytics_track", }, }, store: { diff --git a/src/ipc/ipc-estimate-utils.js b/src/ipc/ipc-estimate-utils.js index 823ed6a..ef7e7dc 100644 --- a/src/ipc/ipc-estimate-utils.js +++ b/src/ipc/ipc-estimate-utils.js @@ -5,7 +5,7 @@ import client from "../graphql/GraphQLClient"; import { INSERT_NEW_JOB, QUERY_JOB_BY_CLM_NO, - UPDATE_JOB, + UPDATE_JOB } from "../graphql/jobs.queries"; import { QUERY_GROUPS_BY_MAKE_TYPE } from "../graphql/veh_group.queries"; import { store } from "../redux/store"; @@ -15,8 +15,6 @@ export async function UpsertEstimate(job) { const shopId = store.getState().user.bodyshop.id; logger.info("Beginning Upserting job from Renderer."); const parsedYr = parseInt(job.v_model_yr); - console.log("UpsertEstimate -> parsedYr", parsedYr); - logger.info( moment(job.loss_date).year() - (parsedYr >= 0 ? 2000 + parsedYr : 1900 + parsedYr) @@ -50,13 +48,11 @@ export async function UpsertEstimate(job) { }); delete job.joblines; - const updatedJob = await client.mutate({ + await client.mutate({ mutation: UPDATE_JOB, variables: { jobId: existingJobs.data.jobs[0].id, job: job }, }); logger.info("Job updated succesfully."); - - console.log("UpsertEstimate -> updatedJob", updatedJob); } else { logger.info("Attemping to insert job record."); diff --git a/src/redux/application/application.sagas.js b/src/redux/application/application.sagas.js index c69a008..c56bb37 100644 --- a/src/redux/application/application.sagas.js +++ b/src/redux/application/application.sagas.js @@ -1,4 +1,4 @@ -import { all, call, takeLatest, select, put } from "redux-saga/effects"; +import { all, call, put, select, takeLatest } from "redux-saga/effects"; import GetJobTarget from "../../util/GetJobTarget"; import { setSelectedJobTargetPcSuccess } from "./application.actions"; import ApplicationActionTypes from "./application.types"; diff --git a/src/redux/reporting/reporting.sagas.js b/src/redux/reporting/reporting.sagas.js index 5624d96..a56f211 100644 --- a/src/redux/reporting/reporting.sagas.js +++ b/src/redux/reporting/reporting.sagas.js @@ -1,20 +1,21 @@ -import { all, call, takeLatest, select, put } from "redux-saga/effects"; +import Dinero from "dinero.js"; +import { all, call, put, select, takeLatest } from "redux-saga/effects"; +import client from "../../graphql/GraphQLClient"; +import { REPORTING_GET_JOBS } from "../../graphql/reporting.queries"; +import ipcTypes from "../../ipc.types"; +import { + CalculateJobRpsDollars, + CalculateJobRpsPc +} from "../../util/CalculateJobRps"; +import GetJobTarget from "../../util/GetJobTarget"; import { calculateScorecard, setReportingData, - setScoreCard, + 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; +const { log, ipcRenderer } = window; export function* onQueryReportData() { yield takeLatest( @@ -52,71 +53,81 @@ export function* onCalculateScoreCard() { ); } export function* handleCalculateScoreCard({ payload: jobs }) { - console.log("jobs", jobs); - const targets = yield select((state) => state.user.bodyshop.targets); + try { + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "CALCULATE_SCORECARD", + }); - const scoreCard = { - shopRpsTotalDollars: Dinero(), - shopRpsExpectedDollars: Dinero(), - varianceDollars: null, - variancePc: 0, - allJobsSumDbPrice: Dinero(), - allJobsSumActPrice: Dinero(), - currentRpsPc: 0, - targetRpsPc: 0, - }; + const targets = yield select((state) => state.user.bodyshop.targets); - //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, + const scoreCard = { + shopRpsTotalDollars: Dinero(), + shopRpsExpectedDollars: Dinero(), + varianceDollars: null, + variancePc: 0, + allJobsSumDbPrice: Dinero(), + allJobsSumActPrice: Dinero(), + currentRpsPc: 0, + targetRpsPc: 0, }; - }); - scoreCard.varianceDollars = scoreCard.shopRpsTotalDollars.subtract( - scoreCard.shopRpsExpectedDollars - ); + //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.variancePc = - scoreCard.varianceDollars.getAmount() / - scoreCard.shopRpsExpectedDollars.getAmount(); + scoreCard.allJobsSumDbPrice = scoreCard.allJobsSumDbPrice.add(dbPriceSum); + scoreCard.allJobsSumActPrice = scoreCard.allJobsSumActPrice.add( + actPriceSum + ); - 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)); + //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)); + } catch (error) { + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "CALCULATE_SCORE_CARD_ERROR", + error: error, + }); + } } export function* reportingSagas() { diff --git a/src/redux/user/user.reducer.js b/src/redux/user/user.reducer.js index 0f62651..840eeb2 100644 --- a/src/redux/user/user.reducer.js +++ b/src/redux/user/user.reducer.js @@ -47,7 +47,7 @@ const userReducer = (state = INITIAL_STATE, action) => { return { ...state, currentUser: action.payload, - loingLoading: false, + loginLoading: false, error: null, }; case UserActionTypes.SIGN_OUT_SUCCESS: diff --git a/src/redux/user/user.sagas.js b/src/redux/user/user.sagas.js index 9880553..fa04b4a 100644 --- a/src/redux/user/user.sagas.js +++ b/src/redux/user/user.sagas.js @@ -1,3 +1,5 @@ +import { message } from "antd"; +import LogRocket from "logrocket"; import { all, call, put, takeLatest } from "redux-saga/effects"; import { auth, @@ -18,8 +20,6 @@ import { signOutSuccess, unauthorizedUser, updateUserDetailsSuccess, - validatePasswordResetFailure, - validatePasswordResetSuccess, } from "./user.actions"; import UserActionTypes from "./user.types"; @@ -30,6 +30,10 @@ export function* onEmailSignInStart() { } export function* signInWithEmail({ payload: { email, password } }) { try { + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "SIGN_IN_ATTEMPT", + email: email, + }); const { user } = yield auth.signInWithEmailAndPassword(email, password); const result = yield client.mutate({ @@ -50,13 +54,16 @@ export function* signInWithEmail({ payload: { email, password } }) { yield put(signInFailure(JSON.stringify(result.errors))); } } catch (error) { - yield put(signInFailure(error)); + yield put( + signInFailure({ ...error, messagePretty: ErrorFormatter(error.code) }) + ); } } export function* onCheckUserSession() { yield takeLatest(UserActionTypes.CHECK_USER_SESSION, isUserAuthenticated); } + export function* isUserAuthenticated() { try { const user = yield getCurrentUser(); @@ -78,9 +85,11 @@ export function* isUserAuthenticated() { yield put(signInFailure(error)); } } + export function* onSignOutStart() { yield takeLatest(UserActionTypes.SIGN_OUT_START, signOutStart); } + export function* signOutStart() { try { ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.stop); @@ -112,7 +121,15 @@ export function* onSignInSuccess() { export function* signInSuccessSaga({ payload }) { //Query for the Correct Bodyshop + ipcRenderer.send(ipcTypes.default.app.toMain.setUserName, payload.email); + LogRocket.identify(payload.email, { + email: payload.email, + }); + ipcRenderer.send(ipcTypes.default.app.toMain.track, { + event: "SIGN_IN_SUCCESS", + email: payload.email, + }); const shop = yield client.query({ query: QUERY_BODYSHOP }); if (shop.data.bodyshops.length > 0) { yield put(setBodyshop(shop.data.bodyshops[0])); @@ -138,32 +155,14 @@ export function* onSendPasswordResetStart() { } export function* sendPasswordResetEmail({ payload }) { try { - yield auth.sendPasswordResetEmail(payload, { - url: "https://imex.online/passwordreset", - }); + yield auth.sendPasswordResetEmail(payload); yield put(sendPasswordResetSuccess()); + message.success("Password reset sent succesfully."); } catch (error) { yield put(sendPasswordResetFailure(error.message)); } } - -export function* onValidatePasswordResetStart() { - yield takeLatest( - UserActionTypes.VALIDATE_PASSWORD_RESET_START, - validatePasswordResetStart - ); -} -export function* validatePasswordResetStart({ payload: { password, code } }) { - try { - yield auth.confirmPasswordReset(code, password); - yield put(validatePasswordResetSuccess()); - } catch (error) { - console.log("function*validatePasswordResetStart -> error", error); - yield put(validatePasswordResetFailure(error.message)); - } -} - export function* userSagas() { yield all([ call(onEmailSignInStart), @@ -172,6 +171,18 @@ export function* userSagas() { call(onUpdateUserDetails), call(onSignInSuccess), call(onSendPasswordResetStart), - call(onValidatePasswordResetStart), ]); } + +const ErrorFormatter = (code) => { + switch (code) { + case "auth/invalid-email": + return "Please enter a valid email."; + case "auth/user-not-found": + return "A user does not exist with that email."; + case "auth/wrong-password": + return "The email or password is incorrect."; + default: + return code; + } +};