From 45209bd9e4a654f16352b3064dd0c0e6c7bc6753 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 20 Mar 2025 12:50:47 -0700 Subject: [PATCH] Almost matching export. --- package-lock.json | 105 +++++++++++++++++- package.json | 4 +- src/main/decoder/decode-ad1.interface.ts | 2 +- src/main/decoder/decode-ad1.ts | 21 ++-- src/main/decoder/decode-ad2.ts | 28 ++--- src/main/decoder/decode-env.interface.ts | 5 +- src/main/decoder/decode-env.ts | 4 +- src/main/decoder/decode-lin.interface.ts | 8 +- src/main/decoder/decode-lin.ts | 10 +- src/main/decoder/decode-pfh.ts | 18 ++-- src/main/decoder/decode-pfm.interface.ts | 4 + src/main/decoder/decode-pfm.ts | 17 ++- src/main/decoder/decode-pfo.interface.ts | 6 +- src/main/decoder/decode-pfo.ts | 6 +- src/main/decoder/decode-pfp.interface.ts | 6 +- src/main/decoder/decode-pfp.ts | 14 ++- src/main/decoder/decode-pft.interface.ts | 6 +- src/main/decoder/decode-pft.ts | 6 +- src/main/decoder/decode-stl.interface.ts | 5 +- src/main/decoder/decode-stl.ts | 10 +- src/main/decoder/decode-ttl.ts | 4 +- src/main/decoder/decode-veh.interface.ts | 9 +- src/main/decoder/decode-veh.ts | 12 +-- src/main/decoder/decoder.ts | 40 +++++-- src/main/index.ts | 132 ++++++++++++++++++++++- src/main/ipc/ipcMainConfig.ts | 19 ++-- src/main/store/store.ts | 3 + src/renderer/src/App.tsx | 44 ++++---- src/renderer/src/redux/app.slice.ts | 37 +++++++ src/renderer/src/redux/redux-store.ts | 13 +++ src/renderer/src/redux/reduxHooks.ts | 8 ++ src/util/deepLowercaseKeys.ts | 4 +- 32 files changed, 490 insertions(+), 120 deletions(-) create mode 100644 src/renderer/src/redux/app.slice.ts create mode 100644 src/renderer/src/redux/redux-store.ts create mode 100644 src/renderer/src/redux/reduxHooks.ts diff --git a/package-lock.json b/package-lock.json index 4f02e26..2c13023 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "bodyshop-desktop", - "version": "1.0.0", + "version": "0.0.1-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bodyshop-desktop", - "version": "1.0.0", + "version": "0.0.1-alpha.1", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", + "@reduxjs/toolkit": "^2.6.1", "chokidar": "^4.0.3", "dbffile": "^1.12.0", "electron-log": "^5.3.2", @@ -20,7 +21,8 @@ "lodash": "^4.17.21", "playwright": "^1.51.0", "react-error-boundary": "^5.0.0", - "react-i18next": "^15.4.1" + "react-i18next": "^15.4.1", + "react-redux": "^9.2.0" }, "devDependencies": { "@ant-design/v5-patch-for-react-19": "^1.0.3", @@ -2900,6 +2902,30 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.1.tgz", + "integrity": "sha512-SSlIqZNYhqm/oMkXbtofwZSt9lrncblzo6YcZ9zoX+zLngRBrCOjK4lNLdkNucJF58RHOWrD9txT3bT3piH7Zw==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.35.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", @@ -3352,7 +3378,7 @@ "version": "19.0.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -3377,6 +3403,12 @@ "@types/node": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/verror": { "version": "1.10.11", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", @@ -5217,7 +5249,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -7616,6 +7648,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10583,6 +10625,29 @@ "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -10684,6 +10749,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10771,6 +10851,12 @@ "url": "https://github.com/sponsors/jet2jet" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -12115,6 +12201,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utf8-byte-length": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", diff --git a/package.json b/package.json index 55ff2fd..f789246 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^4.0.0", + "@reduxjs/toolkit": "^2.6.1", "chokidar": "^4.0.3", "dbffile": "^1.12.0", "electron-log": "^5.3.2", @@ -32,7 +33,8 @@ "lodash": "^4.17.21", "playwright": "^1.51.0", "react-error-boundary": "^5.0.0", - "react-i18next": "^15.4.1" + "react-i18next": "^15.4.1", + "react-redux": "^9.2.0" }, "devDependencies": { "@ant-design/v5-patch-for-react-19": "^1.0.3", diff --git a/src/main/decoder/decode-ad1.interface.ts b/src/main/decoder/decode-ad1.interface.ts index 70ab302..d6a72c7 100644 --- a/src/main/decoder/decode-ad1.interface.ts +++ b/src/main/decoder/decode-ad1.interface.ts @@ -28,7 +28,7 @@ export interface DecodedAd1 { ded_amt?: string; ded_status?: string; asgn_no?: string; - asgn_date?: string; + asgn_date?: Date | string; asgn_type?: string; // Claim information diff --git a/src/main/decoder/decode-ad1.ts b/src/main/decoder/decode-ad1.ts index 64a8dde..cfe6e92 100644 --- a/src/main/decoder/decode-ad1.ts +++ b/src/main/decoder/decode-ad1.ts @@ -3,6 +3,7 @@ import log from "electron-log/main"; import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import errorTypeCheck from "../../util/errorTypeCheck"; +import store from "../store/store"; import { DecodedAd1, OwnerRecordInterface } from "./decode-ad1.interface"; const DecodeAD1 = async ( @@ -26,7 +27,9 @@ const DecodeAD1 = async ( //AD1 will always have only 1 row. //Commented lines have been cross referenced with existing partner fields. - + const d = rawDBFRecord[0].ASGN_DATE; + console.log(d); + console.log(typeof rawDBFRecord[0].ASGN_DATE); const rawAd1Data: DecodedAd1 = deepLowerCaseKeys( _.pick(rawDBFRecord[0], [ //TODO: Add typings for EMS File Formats. @@ -149,6 +152,11 @@ const DecodeAD1 = async ( //Copy specific logic for manipulation. //If ownr_ph1 is missing, use ownr_ph2 + if (rawAd1Data.asgn_date) { + const newAsgnDate = new Date(rawAd1Data.asgn_date); + rawAd1Data.asgn_date = newAsgnDate.toISOString().split("T")[0]; + } + if (!rawAd1Data.ownr_ph1) { rawAd1Data.ownr_ph1 = rawAd1Data.ownr_ph2; } @@ -161,7 +169,8 @@ const DecodeAD1 = async ( _.isEmpty(rawAd1Data.ownr_co_nm) ) { //They're all empty. Using the insured information as a fallback. - // //Build up the owner record to insert it alongside the job. + // Build up the owner record to insert it alongside the job. + //TODO: Verify that this should be the insured, and not the claimant. ownerRecord = { ownr_ln: rawAd1Data.insd_ln, ownr_fn: rawAd1Data.insd_fn, @@ -176,8 +185,7 @@ const DecodeAD1 = async ( ownr_ph1: rawAd1Data.insd_ph1, ownr_ph2: rawAd1Data.insd_ph2, ownr_ea: rawAd1Data.insd_ea, - - shopid: "UUID", //TODO: Need to add the shop uuid to this set of functions. + shopid: store.get("app.bodyshop.id"), }; } else { //Use the owner information. @@ -195,10 +203,11 @@ const DecodeAD1 = async ( ownr_ph1: rawAd1Data.ownr_ph1, ownr_ph2: rawAd1Data.ownr_ph2, ownr_ea: rawAd1Data.ownr_ea, - shopid: "UUID", //TODO: Need to add the shop uuid to this set of functions. + shopid: store.get("app.bodyshop.id"), }; } - + const s = store.get("app"); + console.log(s); return { ...rawAd1Data, owner: { data: ownerRecord } }; }; export default DecodeAD1; diff --git a/src/main/decoder/decode-ad2.ts b/src/main/decoder/decode-ad2.ts index e21426f..557a489 100644 --- a/src/main/decoder/decode-ad2.ts +++ b/src/main/decoder/decode-ad2.ts @@ -30,24 +30,24 @@ const DecodeAD2 = async ( const rawAd2Data: DecodedAD2 = deepLowerCaseKeys( _.pick(rawDBFRecord[0], [ //TODO: Add typings for EMS File Formats. - "CLMT_LN", - "CLMT_FN", - "CLMT_TITLE", - "CLMT_CO_NM", - "CLMT_ADDR1", - "CLMT_ADDR2", - "CLMT_CITY", - "CLMT_ST", - "CLMT_ZIP", - "CLMT_CTRY", - "CLMT_PH1", + // "CLMT_LN", //TODO: This claimant info shouldnt be passed back. Just for the owner info. + // "CLMT_FN", + // "CLMT_TITLE", + // "CLMT_CO_NM", + // "CLMT_ADDR1", + // "CLMT_ADDR2", + // "CLMT_CITY", + // "CLMT_ST", + // "CLMT_ZIP", + // "CLMT_CTRY", + // "CLMT_PH1", //"CLMT_PH1X", - "CLMT_PH2", + //"CLMT_PH2", //"CLMT_PH2X", //"CLMT_FAX", //"CLMT_FAXX", - "CLMT_EA", - "EST_CO_ID", + //"CLMT_EA", + //"EST_CO_ID", "EST_CO_NM", "EST_ADDR1", "EST_ADDR2", diff --git a/src/main/decoder/decode-env.interface.ts b/src/main/decoder/decode-env.interface.ts index f120de1..0148524 100644 --- a/src/main/decoder/decode-env.interface.ts +++ b/src/main/decoder/decode-env.interface.ts @@ -1,4 +1,5 @@ export interface DecodedEnv { - est_system: string; - estfile_id: string; + est_system?: string; + estfile_id?: string; + ciecaid?: string; } diff --git a/src/main/decoder/decode-env.ts b/src/main/decoder/decode-env.ts index 3f4b305..00a91a7 100644 --- a/src/main/decoder/decode-env.ts +++ b/src/main/decoder/decode-env.ts @@ -30,10 +30,12 @@ const DecodeEnv = async ( _.pick(rawDBFRecord[0], [ //TODO: Add typings for EMS File Formats. //TODO: Several of these fields will fail. Should extend schema to capture them. - "EST_SYSTEM", + //"EST_SYSTEM", "ESTFILE_ID", ]) ); + rawEnvData.ciecaid = rawEnvData.estfile_id; + delete rawEnvData.estfile_id; //Apply business logic transfomrations. diff --git a/src/main/decoder/decode-lin.interface.ts b/src/main/decoder/decode-lin.interface.ts index 345222b..295dadb 100644 --- a/src/main/decoder/decode-lin.interface.ts +++ b/src/main/decoder/decode-lin.interface.ts @@ -1,4 +1,4 @@ -export interface DecodedLin { +export interface DecodedLinLine { line_no?: string; line_ind?: string; line_ref?: string; @@ -46,3 +46,9 @@ export interface DecodedLin { bett_tax?: boolean; op_code_desc?: string; } + +export interface DecodedLin { + joblines: { + data: DecodedLinLine[]; + }; +} diff --git a/src/main/decoder/decode-lin.ts b/src/main/decoder/decode-lin.ts index 339ea47..166eba0 100644 --- a/src/main/decoder/decode-lin.ts +++ b/src/main/decoder/decode-lin.ts @@ -2,12 +2,12 @@ import { DBFFile } from "dbffile"; import log from "electron-log/main"; import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; -import { DecodedLin } from "./decode-lin.interface"; +import { DecodedLin, DecodedLinLine } from "./decode-lin.interface"; import errorTypeCheck from "../../util/errorTypeCheck"; const DecodeLin = async ( extensionlessFilePath: string -): Promise => { +): Promise => { let dbf: DBFFile | null = null; try { dbf = await DBFFile.open(`${extensionlessFilePath}.LIN`); @@ -26,8 +26,8 @@ const DecodeLin = async ( //AD2 will always have only 1 row. //Commented lines have been cross referenced with existing partner fields. - const rawLinData: DecodedLin[] = rawDBFRecord.map((record) => { - const singleLineData: DecodedLin = deepLowerCaseKeys( + const rawLinData: DecodedLinLine[] = rawDBFRecord.map((record) => { + const singleLineData: DecodedLinLine = deepLowerCaseKeys( _.pick(record, [ //TODO: Add typings for EMS File Formats. "LINE_NO", @@ -99,6 +99,6 @@ const DecodeLin = async ( //Apply business logic transfomrations. //We don't have an inspection date, we instead have `date_estimated` - return rawLinData; + return { joblines: { data: rawLinData } }; }; export default DecodeLin; diff --git a/src/main/decoder/decode-pfh.ts b/src/main/decoder/decode-pfh.ts index 7861b0d..6e53764 100644 --- a/src/main/decoder/decode-pfh.ts +++ b/src/main/decoder/decode-pfh.ts @@ -31,7 +31,7 @@ const DecodePfh = async ( _.pick(rawDBFRecord[0], [ //TODO: Add typings for EMS File Formats. //TODO: Several of these fields will fail. Should extend schema to capture them. - "ID_PRO_NAM", //Remove + //"ID_PRO_NAM", //Remove "TAX_PRETHR", "TAX_THRAMT", //Remove "TAX_PSTTHR", @@ -48,7 +48,7 @@ const DecodePfh = async ( "ADJ_G_DISC", "ADJ_TOWDIS", "ADJ_STRDIS", - "ADJ_BTR_IN", //Remove + //"ADJ_BTR_IN", //Remove "TAX_PREDIS", ]) ); @@ -57,13 +57,13 @@ const DecodePfh = async ( //Standardize some of the numbers and divide by 100. - rawPfhData.tax_prethr = rawPfhData.tax_prethr ?? 0 / 100; - rawPfhData.tax_pstthr = rawPfhData.tax_pstthr ?? 0 / 100; - rawPfhData.tax_tow_rt = rawPfhData.tax_tow_rt ?? 0 / 100; - rawPfhData.tax_str_rt = rawPfhData.tax_str_rt ?? 0 / 100; - rawPfhData.tax_sub_rt = rawPfhData.tax_sub_rt ?? 0 / 100; - rawPfhData.tax_lbr_rt = rawPfhData.tax_lbr_rt ?? 0 / 100; - rawPfhData.federal_tax_rate = rawPfhData.tax_gst_rt ?? 0 / 100; + rawPfhData.tax_prethr = (rawPfhData.tax_prethr ?? 0) / 100; + rawPfhData.tax_pstthr = (rawPfhData.tax_pstthr ?? 0) / 100; + rawPfhData.tax_tow_rt = (rawPfhData.tax_tow_rt ?? 0) / 100; + rawPfhData.tax_str_rt = (rawPfhData.tax_str_rt ?? 0) / 100; + rawPfhData.tax_sub_rt = (rawPfhData.tax_sub_rt ?? 0) / 100; + rawPfhData.tax_lbr_rt = (rawPfhData.tax_lbr_rt ?? 0) / 100; + rawPfhData.federal_tax_rate = (rawPfhData.tax_gst_rt ?? 0) / 100; delete rawPfhData.tax_gst_rt; return rawPfhData; diff --git a/src/main/decoder/decode-pfm.interface.ts b/src/main/decoder/decode-pfm.interface.ts index 283538b..85ba3bb 100644 --- a/src/main/decoder/decode-pfm.interface.ts +++ b/src/main/decoder/decode-pfm.interface.ts @@ -42,5 +42,9 @@ export interface JobMaterialRateFields { } export interface DecodedPfm extends JobMaterialRateFields { + materials: { + mapa?: DecodedPfmLine; + mash?: DecodedPfmLine; + }; cieca_pfm: DecodedPfmLine[]; } diff --git a/src/main/decoder/decode-pfm.ts b/src/main/decoder/decode-pfm.ts index 35461ef..937d80b 100644 --- a/src/main/decoder/decode-pfm.ts +++ b/src/main/decoder/decode-pfm.ts @@ -3,12 +3,12 @@ import log from "electron-log/main"; import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import errorTypeCheck from "../../util/errorTypeCheck"; +import YNBoolConverter from "../../util/ynBoolConverter"; import { DecodedPfm, DecodedPfmLine, JobMaterialRateFields, } from "./decode-pfm.interface"; -import YNBoolConverter from "../../util/ynBoolConverter"; const DecodePfm = async ( extensionlessFilePath: string @@ -98,7 +98,7 @@ const DecodePfm = async ( if (mapaLine) { jobMaterialRates.rate_mapa = mapaLine.cal_lbrrte || mapaLine.cal_prethr || 0; - jobMaterialRates.tax_paint_mat_rt = mapaLine.mat_taxp ?? 0 / 100; + jobMaterialRates.tax_paint_mat_rt = (mapaLine.mat_taxp ?? 0) / 100; } const mashLine: DecodedPfmLine | undefined = rawPfmData.find( @@ -107,7 +107,7 @@ const DecodePfm = async ( if (mashLine) { jobMaterialRates.rate_mash = mashLine.cal_lbrrte || mashLine.cal_prethr || 0; - jobMaterialRates.tax_shop_mat_rt = mashLine.mat_taxp ?? 0 / 100; + jobMaterialRates.tax_shop_mat_rt = (mashLine.mat_taxp ?? 0) / 100; } const mahwLine: DecodedPfmLine | undefined = rawPfmData.find( @@ -116,7 +116,7 @@ const DecodePfm = async ( if (mahwLine) { jobMaterialRates.rate_mahw = mahwLine.cal_lbrrte || mahwLine.cal_prethr || 0; - jobMaterialRates.tax_levies_rt = mahwLine.mat_taxp ?? 0 / 100; + jobMaterialRates.tax_levies_rt = (mahwLine.mat_taxp ?? 0) / 100; } const additionalMaterials = ["MA2S", "MA2T", "MA3S", "MACS", "MABL"]; @@ -133,7 +133,14 @@ const DecodePfm = async ( //Apply business logic transfomrations. //We don't have an inspection date, we instead have `date_estimated` - return { ...jobMaterialRates, cieca_pfm: rawPfmData }; + return { + ...jobMaterialRates, + materials: { + mash: mashLine, + mapa: mapaLine, + }, + cieca_pfm: rawPfmData, + }; }; export default DecodePfm; diff --git a/src/main/decoder/decode-pfo.interface.ts b/src/main/decoder/decode-pfo.interface.ts index 8c12862..07c256e 100644 --- a/src/main/decoder/decode-pfo.interface.ts +++ b/src/main/decoder/decode-pfo.interface.ts @@ -1,4 +1,4 @@ -export interface DecodedPfo { +export interface DecodedPfoLine { tx_tow_ty?: string; tow_t_ty1?: string; tow_t_in1?: boolean; @@ -26,3 +26,7 @@ export interface DecodedPfo { stor_t_ty6?: string; stor_t_in6?: boolean; } + +export interface DecodedPfo { + cieca_pfo: DecodedPfoLine; +} diff --git a/src/main/decoder/decode-pfo.ts b/src/main/decoder/decode-pfo.ts index 6637c2a..3976612 100644 --- a/src/main/decoder/decode-pfo.ts +++ b/src/main/decoder/decode-pfo.ts @@ -3,8 +3,8 @@ import log from "electron-log/main"; import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import errorTypeCheck from "../../util/errorTypeCheck"; -import { DecodedPfo } from "./decode-pfo.interface"; import YNBoolConverter from "../../util/ynBoolConverter"; +import { DecodedPfo, DecodedPfoLine } from "./decode-pfo.interface"; const DecodePfo = async ( extensionlessFilePath: string @@ -28,7 +28,7 @@ const DecodePfo = async ( //PFO will always have only 1 row. //Commented lines have been cross referenced with existing partner fields. - const rawPfoData: DecodedPfo = YNBoolConverter( + const rawPfoData: DecodedPfoLine = YNBoolConverter( deepLowerCaseKeys( _.pick(rawDBFRecord[0], [ //TODO: Add typings for EMS File Formats. @@ -64,6 +64,6 @@ const DecodePfo = async ( //Apply business logic transfomrations. - return rawPfoData; + return { cieca_pfo: rawPfoData }; }; export default DecodePfo; diff --git a/src/main/decoder/decode-pfp.interface.ts b/src/main/decoder/decode-pfp.interface.ts index 22cc866..d0f15a8 100644 --- a/src/main/decoder/decode-pfp.interface.ts +++ b/src/main/decoder/decode-pfp.interface.ts @@ -17,7 +17,7 @@ export interface DecodedPfpLine { prt_tx_in5: boolean; } -export interface DecodedPfp { +export interface DecodedPfpLinesByType { PAA: DecodedPfpLine; PAC: DecodedPfpLine; PAL: DecodedPfpLine; @@ -31,3 +31,7 @@ export interface DecodedPfp { PASL: DecodedPfpLine; PAT: DecodedPfpLine; } + +export interface DecodedPfp { + parts_tax_rates: DecodedPfpLinesByType; +} diff --git a/src/main/decoder/decode-pfp.ts b/src/main/decoder/decode-pfp.ts index 6ff89de..aae3cdf 100644 --- a/src/main/decoder/decode-pfp.ts +++ b/src/main/decoder/decode-pfp.ts @@ -4,7 +4,11 @@ import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import errorTypeCheck from "../../util/errorTypeCheck"; import YNBoolConverter from "../../util/ynBoolConverter"; -import { DecodedPfp, DecodedPfpLine } from "./decode-pfp.interface"; +import { + DecodedPfp, + DecodedPfpLine, + DecodedPfpLinesByType, +} from "./decode-pfp.interface"; const DecodePfp = async ( extensionlessFilePath: string @@ -57,15 +61,15 @@ const DecodePfp = async ( //Apply business logic transfomrations. //Convert array of lines to a hash object. - const parsedPfpFile: DecodedPfp = rawPfpData.reduce( - (acc: DecodedPfp, line: DecodedPfpLine) => { + const parsedPfpFile: DecodedPfpLinesByType = rawPfpData.reduce( + (acc: DecodedPfpLinesByType, line: DecodedPfpLine) => { acc[line.prt_type] = line; return acc; }, - {} as DecodedPfp + {} as DecodedPfpLinesByType ); - return parsedPfpFile; + return { parts_tax_rates: parsedPfpFile }; }; export default DecodePfp; diff --git a/src/main/decoder/decode-pft.interface.ts b/src/main/decoder/decode-pft.interface.ts index 16b5fd7..664171d 100644 --- a/src/main/decoder/decode-pft.interface.ts +++ b/src/main/decoder/decode-pft.interface.ts @@ -2,7 +2,7 @@ * Interface representing decoded data from a PFT file * Contains tax type information with up to 6 tax types and 5 tiers each */ -export interface DecodedPft { +export interface DecodedPftLine { // Tax Type 1 tax_type1?: string; ty1_tier1?: number; @@ -141,3 +141,7 @@ export interface DecodedPft { ty6_rate5?: number; ty6_sur5?: number; } + +export interface DecodedPft { + cieca_pft: DecodedPftLine; +} diff --git a/src/main/decoder/decode-pft.ts b/src/main/decoder/decode-pft.ts index ac5c0dc..3039af8 100644 --- a/src/main/decoder/decode-pft.ts +++ b/src/main/decoder/decode-pft.ts @@ -3,7 +3,7 @@ import log from "electron-log/main"; import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import errorTypeCheck from "../../util/errorTypeCheck"; -import { DecodedPft } from "./decode-pft.interface"; +import { DecodedPft, DecodedPftLine } from "./decode-pft.interface"; const DecodePft = async ( extensionlessFilePath: string @@ -25,7 +25,7 @@ const DecodePft = async ( //PFT will always have only 1 row. //Commented lines have been cross referenced with existing partner fields. - const rawPftData: DecodedPft = deepLowerCaseKeys( + const rawPftData: DecodedPftLine = deepLowerCaseKeys( _.pick(rawDBFRecord[0], [ //TODO: Add typings for EMS File Formats. "TAX_TYPE1", //The below is is taken from a CCC estimate. Will require validation to ensure it is also accurate for Audatex/Mitchell @@ -160,6 +160,6 @@ const DecodePft = async ( //Apply business logic transfomrations. //We don't have an inspection date, we instead have `date_estimated` - return rawPftData; + return { cieca_pft: rawPftData }; }; export default DecodePft; diff --git a/src/main/decoder/decode-stl.interface.ts b/src/main/decoder/decode-stl.interface.ts index 71d15b4..69198d4 100644 --- a/src/main/decoder/decode-stl.interface.ts +++ b/src/main/decoder/decode-stl.interface.ts @@ -1,4 +1,4 @@ -export interface DecodedStl { +export interface DecodedStlLine { ttl_type?: string; ttl_typecd?: string; t_amt?: number; @@ -18,3 +18,6 @@ export interface DecodedStl { ttl_hrs?: number; ttl_amt?: number; } +export interface DecodedStl { + cieca_stl: DecodedStlLine[]; +} diff --git a/src/main/decoder/decode-stl.ts b/src/main/decoder/decode-stl.ts index 98dc656..3ad3c5a 100644 --- a/src/main/decoder/decode-stl.ts +++ b/src/main/decoder/decode-stl.ts @@ -3,11 +3,11 @@ import log from "electron-log/main"; import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import errorTypeCheck from "../../util/errorTypeCheck"; -import { DecodedStl } from "./decode-stl.interface"; +import { DecodedStl, DecodedStlLine } from "./decode-stl.interface"; const DecodeStl = async ( extensionlessFilePath: string -): Promise => { +): Promise => { let dbf: DBFFile | null = null; try { dbf = await DBFFile.open(`${extensionlessFilePath}.STL`); @@ -25,8 +25,8 @@ const DecodeStl = async ( //AD2 will always have only 1 row. //Commented lines have been cross referenced with existing partner fields. - const rawStlData: DecodedStl[] = rawDBFRecord.map((record) => { - const singleLineData: DecodedStl = deepLowerCaseKeys( + const rawStlData: DecodedStlLine[] = rawDBFRecord.map((record) => { + const singleLineData: DecodedStlLine = deepLowerCaseKeys( _.pick(record, [ //TODO: Add typings for EMS File Formats. "TTL_TYPE", @@ -57,6 +57,6 @@ const DecodeStl = async ( //Apply business logic transfomrations. //We don't have an inspection date, we instead have `date_estimated` - return rawStlData; + return { cieca_stl: rawStlData }; }; export default DecodeStl; diff --git a/src/main/decoder/decode-ttl.ts b/src/main/decoder/decode-ttl.ts index 2bd10ec..a3445e1 100644 --- a/src/main/decoder/decode-ttl.ts +++ b/src/main/decoder/decode-ttl.ts @@ -48,8 +48,8 @@ const DecodeTtl = async ( //Apply business logic transfomrations. return { - clm_total: 0, - depreciation_taxes: 0, + clm_total: rawTtlData.g_ttl_amt || 0, + depreciation_taxes: 0, //TODO: Find where this needs to be filled from cieca_ttl: { data: rawTtlData }, }; }; diff --git a/src/main/decoder/decode-veh.interface.ts b/src/main/decoder/decode-veh.interface.ts index 7cccbf5..4bf615e 100644 --- a/src/main/decoder/decode-veh.interface.ts +++ b/src/main/decoder/decode-veh.interface.ts @@ -10,14 +10,17 @@ export interface DecodedVeh { v_model_desc?: string; v_color?: string; kmin?: number; - + area_of_damage?: { + impact1?: string; + impact2?: string; + }; // Complete vehicle data object vehicle: { data: VehicleRecordInterface }; } export interface VehicleRecordInterface { // Area of damage information - area_of_damage: { + area_of_damage?: { impact1?: string; impact2?: string; }; @@ -45,7 +48,7 @@ export interface VehicleRecordInterface { trim_color?: string; v_mldgcode?: string; v_engine?: string; - v_mileage?: string; + v_mileage?: number; //TODO: This can sometimes come in as UNK. v_color?: string; v_tone?: string; v_stage?: string; diff --git a/src/main/decoder/decode-veh.ts b/src/main/decoder/decode-veh.ts index a00206e..902908a 100644 --- a/src/main/decoder/decode-veh.ts +++ b/src/main/decoder/decode-veh.ts @@ -4,6 +4,7 @@ import _ from "lodash"; import deepLowerCaseKeys from "../../util/deepLowercaseKeys"; import { DecodedVeh, VehicleRecordInterface } from "./decode-veh.interface"; import errorTypeCheck from "../../util/errorTypeCheck"; +import store from "../store/store"; const DecodeVeh = async ( extensionlessFilePath: string @@ -85,10 +86,10 @@ const DecodeVeh = async ( delete rawVehData.paint_cd2; delete rawVehData.paint_cd3; - rawVehData.shopid = "UUID"; //TODO: Pass down the shopid for generation. + rawVehData.shopid = store.get("app.bodyshop.id"); //Aggregate the vehicle data to be stamped onto the job record. - const jobVehiclData = { + const jobVehicleData: DecodedVeh = { plate_no: rawVehData.plate_no, plate_st: rawVehData.plate_st, v_vin: rawVehData.v_vin, @@ -97,14 +98,13 @@ const DecodeVeh = async ( v_model_desc: rawVehData.v_model_desc, v_color: rawVehData.v_color, kmin: rawVehData.v_mileage, - }; - - return { - ...jobVehiclData, + area_of_damage: rawVehData.area_of_damage, vehicle: { data: rawVehData, }, }; + + return jobVehicleData; }; export default DecodeVeh; diff --git a/src/main/decoder/decoder.ts b/src/main/decoder/decoder.ts index d1a73bd..806fb7e 100644 --- a/src/main/decoder/decoder.ts +++ b/src/main/decoder/decoder.ts @@ -27,6 +27,7 @@ import DecodeVeh from "./decode-veh"; import { DecodedVeh } from "./decode-veh.interface"; import { DecodedEnv } from "./decode-env.interface"; import DecodeEnv from "./decode-env"; +import fs from "fs"; async function ImportJob(filepath: string): Promise { const parsedFilePath = path.parse(filepath); @@ -43,30 +44,51 @@ async function ImportJob(filepath: string): Promise { const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath); const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath); const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath); - const lin: DecodedLin[] = await DecodeLin(extensionlessFilePath); + const lin: DecodedLin = await DecodeLin(extensionlessFilePath); const pfh: DecodedPfh = await DecodePfh(extensionlessFilePath); const pfl: DecodedPfl = await DecodePfl(extensionlessFilePath); const pft: DecodedPft = await DecodePft(extensionlessFilePath); const pfm: DecodedPfm = await DecodePfm(extensionlessFilePath); const pfo: DecodedPfo = await DecodePfo(extensionlessFilePath); // TODO: This will be the `cieca_pfo` object - const stl: DecodedStl[] = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object + const stl: DecodedStl = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath); const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath); - log.debug("Job Object", { + const jobObject = { ...env, ...ad1, ...ad2, ...veh, - joblines: { data: lin }, + ...lin, ...pfh, - cieca_pfl: pfl, - cieca_pft: pft, - materials: pfm, - cieca_pfo: pfo, + ...pfl, + ...pft, + ...pfm, + ...pfo, ...stl, ...ttl, - parts_tax_rates: pfp, + ...pfp, + }; + + // Save jobObject to a timestamped JSON file + const timestamp = new Date() + .toISOString() + .replace(/:/g, "-") + .replace(/\..+/, ""); + const fileName = `job_${timestamp}_${parsedFilePath.name}.json`; + const logsDir = path.join(process.cwd(), "logs"); + + // Create logs directory if it doesn't exist + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); + } + + const filePath = path.join(logsDir, fileName); + fs.writeFileSync(filePath, JSON.stringify(jobObject, null, 2), "utf8"); + log.info(`Job data saved to: ${filePath}`); + + log.debug("Job Object", { + jobObject, }); } catch (error) { log.error("Error encountered while decoding job. ", errorTypeCheck(error)); diff --git a/src/main/index.ts b/src/main/index.ts index 300a370..0284bcc 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,12 +1,13 @@ import { electronApp, is, optimizer } from "@electron-toolkit/utils"; -import { app, BrowserWindow, shell, webContents } from "electron"; +import { app, BrowserWindow, Menu, shell } from "electron"; import log from "electron-log/main"; -import { join } from "path"; +import path, { join } from "path"; import icon from "../../resources/icon.png?asset"; import ErrorTypeCheck from "../util/errorTypeCheck"; import store from "./store/store"; log.initialize(); +const isMac = process.platform === "darwin"; function createWindow(): void { // Create the browser window. const { width, height, x, y } = store.get("app.windowBounds") as { @@ -22,7 +23,7 @@ function createWindow(): void { x, y, show: false, - autoHideMenuBar: true, + //autoHideMenuBar: true, ...(process.platform === "linux" ? { icon } : {}), webPreferences: { preload: join(__dirname, "../preload/index.js"), @@ -31,6 +32,131 @@ function createWindow(): void { }, }); + const template = [ + // { role: 'appMenu' } + ...(isMac + ? [ + { + label: app.name, + submenu: [ + { role: "about" }, + { type: "separator" }, + { role: "services" }, + { type: "separator" }, + { role: "hide" }, + { role: "hideOthers" }, + { role: "unhide" }, + { type: "separator" }, + { role: "quit" }, + ], + }, + ] + : []), + // { role: 'fileMenu' } + { + label: "File", + submenu: [isMac ? { role: "close" } : { role: "quit" }], + }, + // { role: 'editMenu' } + { + label: "Edit", + submenu: [ + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + ...(isMac + ? [ + { role: "pasteAndMatchStyle" }, + { role: "delete" }, + { role: "selectAll" }, + { type: "separator" }, + { + label: "Speech", + submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }], + }, + ] + : [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]), + ], + }, + // { role: 'viewMenu' } + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { role: "resetZoom" }, + { role: "zoomIn" }, + { role: "zoomOut" }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, + // { role: 'windowMenu' } + { + label: "Window", + submenu: [ + { role: "minimize" }, + { role: "zoom" }, + ...(isMac + ? [ + { type: "separator" }, + { role: "front" }, + { type: "separator" }, + { role: "window" }, + ] + : [{ role: "close" }]), + ], + }, + { + role: "help", + submenu: [ + { + label: "Learn More", + click: async () => { + const { shell } = require("electron"); + await shell.openExternal("https://electronjs.org"); + }, + }, + ], + }, + ...(import.meta.env.DEV + ? [ + { + label: "Development", + submenu: [ + { + label: "Open Log Folder", + click: (): void => { + /* action for item 1 */ + shell.openPath(log.transports.file.getFile().path); + }, + }, + { + label: "Clear Log", + click: (): void => { + log.transports.file.getFile().clear(); + }, + }, + { + label: "Open Config", + click: (): void => { + shell.openPath(path.dirname(store.path)); + }, + }, + ], + }, + ] + : []), + ]; + + const menu: Electron.Menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); + // Store window properties for later const storeWindowState = (): void => { const [width, height] = mainWindow.getSize(); diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts index df91576..52cfedf 100644 --- a/src/main/ipc/ipcMainConfig.ts +++ b/src/main/ipc/ipcMainConfig.ts @@ -49,13 +49,20 @@ if (import.meta.env.DEV) { log.debug("[IPC Debug Functions] Adding Debug Handlers"); ipcMain.on(ipcTypes.toMain.debug.decodeEstimate, async (): Promise => { - const relativeEmsFilepath = `_reference/ems/MPI_1/3698420.ENV`; - // Get the app's root directory and create an absolute path - const rootDir = app.getAppPath(); - const absoluteFilepath = path.join(rootDir, relativeEmsFilepath); + // const relativeEmsFilepath = `_reference/ems/MPI_1/3698420.ENV`; + // // Get the app's root directory and create an absolute path + // const rootDir = app.getAppPath(); + // const absoluteFilepath = path.join(rootDir, relativeEmsFilepath); + // console.log("*** ~ ipcMain.on ~ absoluteFilepath:", absoluteFilepath); - log.debug("[IPC Debug Function] Decode test Estimate", absoluteFilepath); - await ImportJob(absoluteFilepath); + // log.debug("[IPC Debug Function] Decode test Estimate", absoluteFilepath); + // await ImportJob(absoluteFilepath); + + const job2 = `/Users/pfic/Downloads/12285264/2285264.ENV`; + + const job3 = `/Users/pfic/Downloads/14033376/4033376.ENV`; + await ImportJob(job2); + await ImportJob(job3); }); } diff --git a/src/main/store/store.ts b/src/main/store/store.ts index 83e92c8..dc2fd5b 100644 --- a/src/main/store/store.ts +++ b/src/main/store/store.ts @@ -17,6 +17,9 @@ const store = new Store({ y: undefined, }, user: null, + bodyshop: { + id: "6089913a-7522-49e7-8c96-786a488b738d", //TODO: Remove hard coded default. + }, }, }, }); diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 60dcfe0..0d3d864 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -7,16 +7,17 @@ import ipcTypes from "../../util/ipcTypes.json"; import NavigationHeader from "./components/NavigationHeader/Navigationheader"; import SignInForm from "./components/SignInForm/SignInForm"; import { auth } from "./util/firebase"; -import {} from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary"; import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback"; -import Settings from "./components/Settings/Settings"; import Home from "./components/Home/Home"; +import Settings from "./components/Settings/Settings"; +import { Provider } from "react-redux"; +import reduxStore from "./redux/redux-store"; const App: React.FC = () => { const [user, setUser] = useState(null); - auth.onAuthStateChanged((user) => { + auth.onAuthStateChanged((user: User | null) => { setUser(user); //Send back to the main process so that it knows we are authenticated. if (user) { @@ -24,27 +25,30 @@ const App: React.FC = () => { ipcTypes.toMain.authStateChanged, user.toJSON() ); + window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start); } }); return ( - - - - {!user ? ( - - ) : ( - <> - - - } /> - } /> - - - )} - - - + + + + + {!user ? ( + + ) : ( + <> + + + } /> + } /> + + + )} + + + + ); }; diff --git a/src/renderer/src/redux/app.slice.ts b/src/renderer/src/redux/app.slice.ts new file mode 100644 index 0000000..6197b24 --- /dev/null +++ b/src/renderer/src/redux/app.slice.ts @@ -0,0 +1,37 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import type { RootState } from "./redux-store"; + +// Define a type for the slice state +interface AppState { + value: number; +} + +// Define the initial state using that type +const initialState: AppState = { + value: 0, +}; + +export const appSlice = createSlice({ + name: "counter", + // `createSlice` will infer the state type from the `initialState` argument + initialState, + reducers: { + increment: (state) => { + state.value += 1; + }, + decrement: (state) => { + state.value -= 1; + }, + // Use the PayloadAction type to declare the contents of `action.payload` + incrementByAmount: (state, action: PayloadAction) => { + state.value += action.payload; + }, + }, +}); + +export const { increment, decrement, incrementByAmount } = appSlice.actions; + +// Other code such as selectors can use the imported `RootState` type +export const selectCount = (state: RootState): number => state.app.value; + +export default appSlice.reducer; diff --git a/src/renderer/src/redux/redux-store.ts b/src/renderer/src/redux/redux-store.ts new file mode 100644 index 0000000..d43e38f --- /dev/null +++ b/src/renderer/src/redux/redux-store.ts @@ -0,0 +1,13 @@ +import { configureStore } from "@reduxjs/toolkit"; +import appReducer from "./app.slice"; + +const store = configureStore({ + reducer: { app: appReducer }, +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch; +export type AppStore = typeof store; +export default store; diff --git a/src/renderer/src/redux/reduxHooks.ts b/src/renderer/src/redux/reduxHooks.ts new file mode 100644 index 0000000..7b530ac --- /dev/null +++ b/src/renderer/src/redux/reduxHooks.ts @@ -0,0 +1,8 @@ +import type { TypedUseSelectorHook } from "react-redux"; +import { useDispatch, useSelector, useStore } from "react-redux"; +import type { AppDispatch, AppStore, RootState } from "./redux-store"; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch: () => AppDispatch = useDispatch; +export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppStore: () => AppStore = useStore; diff --git a/src/util/deepLowercaseKeys.ts b/src/util/deepLowercaseKeys.ts index ddfc375..732f334 100644 --- a/src/util/deepLowercaseKeys.ts +++ b/src/util/deepLowercaseKeys.ts @@ -22,7 +22,9 @@ function deepLowerCaseKeys(obj: any): T { const lowercaseKey = key.toLowerCase(); result[lowercaseKey] = - typeof value === "object" && value !== null + typeof value === "object" && + value !== null && + Object.keys(value).length > 0 ? deepLowerCaseKeys(value) : value;