Add parsing of AD1 files. Memorize window size and location.

This commit is contained in:
Patrick Fic
2025-03-17 14:27:33 -07:00
parent 10368f8f9e
commit c1949eb5f9
33 changed files with 524 additions and 20 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -18,8 +18,13 @@ nsis:
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
azureSignOptions:
endpoint: https://eus.codesigning.azure.net
certificateProfileName: ImEXRPS
codeSigningAccountName: ImEX
mac:
entitlementsInherit: build/entitlements.mac.plist
category: public.app-category.business
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -39,7 +44,8 @@ appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
provider: s3
bucket: bodyshop-desktop-updater
region: ca-central-1
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

8
package-lock.json generated
View File

@@ -28,6 +28,7 @@
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@playwright/test": "^1.51.0",
"@types/lodash": "^4.17.16",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
@@ -3312,6 +3313,13 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",

View File

@@ -40,6 +40,7 @@
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@playwright/test": "^1.51.0",
"@types/lodash": "^4.17.16",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",

View File

@@ -0,0 +1,147 @@
interface ParsedAD1 {
// Insurance company information
ins_co_id?: string;
ins_co_nm?: string;
ins_addr1?: string;
ins_addr2?: string;
ins_city?: string;
ins_st?: string;
ins_zip?: string;
ins_ctry?: string;
ins_ea?: string;
ins_ph1?: string;
ins_ph1x?: string;
ins_ph2?: string;
ins_ph2x?: string;
ins_fax?: string;
ins_faxx?: string;
ins_ct_ln?: string;
ins_ct_fn?: string;
ins_title?: string;
ins_ct_ph?: string;
ins_ct_phx?: string;
// Policy information
policy_no?: string;
ded_amt?: string;
ded_status?: string;
asgn_no?: string;
asgn_date?: string;
asgn_type?: string;
// Claim information
clm_no?: string;
clm_ofc_id?: string;
clm_ofc_nm?: string;
clm_addr1?: string;
clm_addr2?: string;
clm_city?: string;
clm_st?: string;
clm_zip?: string;
clm_ctry?: string;
clm_ph1?: string;
clm_ph1x?: string;
clm_ph2?: string;
clm_ph2x?: string;
clm_fax?: string;
clm_faxx?: string;
clm_ct_ln?: string;
clm_ct_fn?: string;
clm_title?: string;
clm_ct_ph?: string;
clm_ct_phx?: string;
clm_ea?: string;
// Payment information
payee_nms?: string;
pay_type?: string;
pay_date?: string;
pay_chknm?: string;
pay_amt?: string;
// Agent information
agt_co_id?: string;
agt_co_nm?: string;
agt_addr1?: string;
agt_addr2?: string;
agt_city?: string;
agt_st?: string;
agt_zip?: string;
agt_ctry?: string;
agt_ph1?: string;
agt_ph1x?: string;
agt_ph2?: string;
agt_ph2x?: string;
agt_fax?: string;
agt_faxx?: string;
agt_ct_ln?: string;
agt_ct_fn?: string;
agt_ct_ph?: string;
agt_ct_phx?: string;
agt_ea?: string;
agt_lic_no?: string;
// Loss information
loss_date?: string;
loss_type?: string;
loss_desc?: string;
theft_ind?: string;
cat_no?: string;
tlos_ind?: string;
cust_pr?: string;
loss_cat?: string;
// Insured information
insd_ln?: string;
insd_fn?: string;
insd_title?: string;
insd_co_nm?: string;
insd_addr1?: string;
insd_addr2?: string;
insd_city?: string;
insd_st?: string;
insd_zip?: string;
insd_ctry?: string;
insd_ph1?: string;
insd_ph2?: string;
insd_fax?: string;
insd_faxx?: string;
insd_ea?: string;
// Owner information
ownr_ln?: string;
ownr_fn?: string;
ownr_title?: string;
ownr_co_nm?: string;
ownr_addr1?: string;
ownr_addr2?: string;
ownr_city?: string;
ownr_st?: string;
ownr_zip?: string;
ownr_ctry?: string;
ownr_ph1?: string;
ownr_ph2?: string;
ownr_ea?: string;
// Owner data object - referenced in the code
owner: {
data: OwnerRecordInterface;
};
}
interface OwnerRecordInterface {
ownr_ln: string;
ownr_fn: string;
ownr_title: string;
ownr_co_nm: string;
ownr_addr1: string;
ownr_addr2: string;
ownr_city: string;
ownr_st: string;
ownr_zip: string;
ownr_ctry: string;
ownr_ph1: string;
ownr_ph2: string;
ownr_ea: string;
shopid: string;
}

View File

@@ -0,0 +1,219 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
const DecodeAD1 = async (extensionlessFilePath: string): Promise<ParsedAD1> => {
let dbf;
try {
dbf = await DBFFile.open(`${extensionlessFilePath}A.AD1`);
} catch (error) {
log.error("Error opening AD1 File.", error);
dbf = await DBFFile.open(`${extensionlessFilePath}.AD1`);
log.log("Found AD1 file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any AD1 files at ${extensionlessFilePath}`);
return {
id: 0,
};
}
const rawDBFRecord = await dbf.readRecords(1);
//AD1 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawAd1Data = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"INS_CO_ID",
"INS_CO_NM",
"INS_ADDR1",
"INS_ADDR2",
"INS_CITY",
"INS_ST",
"INS_ZIP",
"INS_CTRY",
"INS_EA",
"POLICY_NO",
"DED_AMT",
"DED_STATUS",
"ASGN_NO",
"ASGN_DATE",
"ASGN_TYPE",
"CLM_NO",
"CLM_OFC_ID",
"CLM_OFC_NM",
"CLM_ADDR1",
"CLM_ADDR2",
"CLM_CITY",
"CLM_ST",
"CLM_ZIP",
"CLM_CTRY",
"CLM_PH1",
"CLM_PH1X",
"CLM_PH2",
"CLM_PH2X",
"CLM_FAX",
"CLM_FAXX",
"CLM_CT_LN",
"CLM_CT_FN",
"CLM_TITLE",
"CLM_CT_PH",
"CLM_CT_PHX",
"CLM_EA",
"PAYEE_NMS",
"PAY_TYPE",
"PAY_DATE",
"PAY_CHKNM",
"PAY_AMT",
"AGT_CO_ID",
"AGT_CO_NM",
"AGT_ADDR1",
"AGT_ADDR2",
"AGT_CITY",
"AGT_ST",
"AGT_ZIP",
"AGT_CTRY",
"AGT_PH1",
"AGT_PH1X",
"AGT_PH2",
"AGT_PH2X",
"AGT_FAX",
"AGT_FAXX",
"AGT_CT_LN",
"AGT_CT_FN",
"AGT_CT_PH",
"AGT_CT_PHX",
"AGT_EA",
"AGT_LIC_NO",
"LOSS_DATE",
"LOSS_TYPE",
"LOSS_DESC",
"THEFT_IND",
"CAT_NO",
"TLOS_IND",
"CUST_PR",
"INSD_LN",
"INSD_FN",
"INSD_TITLE",
"INSD_CO_NM",
"INSD_ADDR1",
"INSD_ADDR2",
"INSD_CITY",
"INSD_ST",
"INSD_ZIP",
"INSD_CTRY",
"INSD_PH1",
//"INSD_PH1X",
"INSD_PH2",
//"INSD_PH2X",
"INSD_FAX",
"INSD_FAXX",
"INSD_EA",
"OWNR_LN",
"OWNR_FN",
"OWNR_TITLE",
"OWNR_CO_NM",
"OWNR_ADDR1",
"OWNR_ADDR2",
"OWNR_CITY",
"OWNR_ST",
"OWNR_ZIP",
"OWNR_CTRY",
"OWNR_PH1",
//"OWNR_PH1X",
"OWNR_PH2",
//"OWNR_PH2X",
//"OWNR_FAX",
//"OWNR_FAXX",
"OWNR_EA",
"INS_PH1",
"INS_PH1X",
"INS_PH2",
"INS_PH2X",
"INS_FAX",
"INS_FAXX",
"INS_CT_LN",
"INS_CT_FN",
"INS_TITLE",
"INS_CT_PH",
"INS_CT_PHX",
"LOSS_CAT",
])
);
//Copy specific logic for manipulation.
//If ownr_ph1 is missing, use ownr_ph2
if (!rawAd1Data.ownr_ph1) {
rawAd1Data.ownr_ph1 = rawAd1Data.ownr_ph2;
}
let ownerRecord: OwnerRecordInterface;
//Check if the owner information is there. If not, use the insured information as a fallback.
if (
_.isEmpty(rawAd1Data.ownr_ln) &&
_.isEmpty(rawAd1Data.ownr_fn) &&
_.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.
ownerRecord = {
ownr_ln: rawAd1Data.insd_ln,
ownr_fn: rawAd1Data.insd_fn,
ownr_title: rawAd1Data.insd_title,
ownr_co_nm: rawAd1Data.insd_co_nm,
ownr_addr1: rawAd1Data.insd_addr1,
ownr_addr2: rawAd1Data.insd_addr2,
ownr_city: rawAd1Data.insd_city,
ownr_st: rawAd1Data.insd_st,
ownr_zip: rawAd1Data.insd_zip,
ownr_ctry: rawAd1Data.insd_ctry,
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.
};
} else {
//Use the owner information.
ownerRecord = {
ownr_ln: rawAd1Data.ownr_ln,
ownr_fn: rawAd1Data.ownr_fn,
ownr_title: rawAd1Data.ownr_title,
ownr_co_nm: rawAd1Data.ownr_co_nm,
ownr_addr1: rawAd1Data.ownr_addr1,
ownr_addr2: rawAd1Data.ownr_addr2,
ownr_city: rawAd1Data.ownr_city,
ownr_st: rawAd1Data.ownr_st,
ownr_zip: rawAd1Data.ownr_zip,
ownr_ctry: rawAd1Data.ownr_ctry,
ownr_ph1: rawAd1Data.ownr_ph1,
ownr_ph2: rawAd1Data.ownr_ph2,
ownr_ea: rawAd1Data.ownr_ea,
shopid: "UUID",
};
}
return { ...rawAd1Data, owner: { data: ownerRecord } };
};
export default DecodeAD1;
interface OwnerRecordInterface {
ownr_ln: string;
ownr_fn: string;
ownr_title: string;
ownr_co_nm: string;
ownr_addr1: string;
ownr_addr2: string;
ownr_city: string;
ownr_st: string;
ownr_zip: string;
ownr_ctry: string;
ownr_ph1: string;
ownr_ph2: string;
ownr_ea: string;
shopid: string;
}

View File

@@ -0,0 +1,19 @@
import log from "electron-log/main";
import path from "path";
import DecodeAD1 from "./decode-ad1";
async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name
);
log.debug("Importing Job", extensionlessFilePath);
const decodedJob = {};
const ad1: ParsedAD1 = await DecodeAD1(extensionlessFilePath);
log.debug("AD1", ad1);
}
export default ImportJob;

View File

@@ -1,7 +1,7 @@
import { _electron as electron } from "playwright";
import { test, expect } from "@playwright/test";
test("example test", async () => {
test("Basic Electron app compilation.", async () => {
const electronApp = await electron.launch({ args: ["."] });
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// This runs in Electron's main process, parameter here is always
@@ -14,8 +14,6 @@ test("example test", async () => {
// Wait for the first BrowserWindow to open
// and return its Page object
const window = await electronApp.firstWindow();
await window.screenshot({ path: "intro.png" });
// close app
await electronApp.close();
});

View File

@@ -4,13 +4,23 @@ import log from "electron-log/main";
import { join } from "path";
import icon from "../../resources/icon.png?asset";
import ErrorTypeCheck from "../util/errorTypeCheck";
import "./store/store";
import store from "./store/store";
log.initialize();
function createWindow(): void {
// Create the browser window.
const { width, height, x, y } = store.get("app.windowBounds") as {
width: number;
height: number;
x: number | undefined;
y: number | undefined;
};
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
width,
height,
x,
y,
show: false,
autoHideMenuBar: true,
...(process.platform === "linux" ? { icon } : {}),
@@ -21,6 +31,17 @@ function createWindow(): void {
},
});
// Store window properties for later
const storeWindowState = (): void => {
const [width, height] = mainWindow.getSize();
const [x, y] = mainWindow.getPosition();
store.set("app.windowBounds", { width, height, x, y });
};
mainWindow.on("resized", storeWindowState);
mainWindow.on("maximize", storeWindowState);
mainWindow.on("unmaximize", storeWindowState);
mainWindow.on("moved", storeWindowState);
mainWindow.on("ready-to-show", () => {
mainWindow.show();
});

View File

@@ -1,6 +1,8 @@
import { ipcMain } from "electron";
import { app, ipcMain } from "electron";
import log from "electron-log/main";
import path from "path";
import ipcTypes from "../../util/ipcTypes.json";
import ImportJob from "../decoder/decoder";
import { StartWatcher } from "../watcher/watcher";
import {
SettingsWatchedFilePathsAdd,
@@ -10,7 +12,7 @@ import {
import { ipcMainHandleAuthStateChanged } from "./ipcMainHandler.user";
// Log all IPC messages and their payloads
const logIpcMessages = () => {
const logIpcMessages = (): void => {
// Get all message types from ipcTypes.toMain
Object.keys(ipcTypes.toMain).forEach((key) => {
const messageType = ipcTypes.toMain[key];
@@ -42,6 +44,24 @@ ipcMain.on(ipcTypes.toMain.test, (payload: any) =>
//Auth handler
ipcMain.on(ipcTypes.toMain.authStateChanged, ipcMainHandleAuthStateChanged);
//Add debug handlers if in development
if (import.meta.env.DEV) {
log.debug("[IPC Debug Functions] Adding Debug Handlers");
ipcMain.on(
ipcTypes.toMain.debug.decodeEstimate,
async (event, payload): Promise<void> => {
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);
log.debug("[IPC Debug Function] Decode test Estimate", absoluteFilepath);
await ImportJob(absoluteFilepath);
}
);
}
//Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.get,

View File

@@ -11,7 +11,15 @@ const store = new Store({
pollingInterval: 30000,
},
},
user: null,
app: {
windowBounds: {
width: 800,
height: 600,
x: undefined,
y: undefined,
},
user: null,
},
},
});

View File

@@ -4,8 +4,9 @@ import log from "electron-log/main";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import ImportJob from "../decoder/decoder";
var watcher: FSWatcher;
let watcher: FSWatcher;
async function StartWatcher(): Promise<boolean> {
const filePaths = store.get("settings.filepaths") || [];
@@ -34,8 +35,7 @@ async function StartWatcher(): Promise<boolean> {
watcher = chokidar.watch(filePaths, {
ignored: (filepath, stats) => {
const p = path.parse(filepath);
return !stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV";
return !stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV"; //Only watch for .ENV files.
},
usePolling: store.get("settings.polling").enabled || false,
interval: store.get("settings.polling").pollingInterval || 1000,
@@ -77,7 +77,7 @@ async function StartWatcher(): Promise<boolean> {
return true;
}
function onWatcherReady() {
function onWatcherReady(): void {
log.info("Watcher ready!");
// const b = BrowserWindow.getAllWindows()[0];
// b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.startSuccess);
@@ -102,8 +102,8 @@ async function StopWatcher(): Promise<boolean> {
return false;
}
async function HandleNewFile(path) {
//await ImportJob(path);
async function HandleNewFile(path): Promise<void> {
await ImportJob(path);
log.log("Received a new file", path);
}

View File

@@ -11,6 +11,7 @@ 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";
const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
@@ -36,7 +37,7 @@ const App: React.FC = () => {
<>
<NavigationHeader />
<Routes>
<Route path="/" element={<div>AuthHome</div>} />
<Route path="/" element={<Home />} />
<Route path="settings" element={<Settings />} />
</Routes>
</>

View File

@@ -0,0 +1,21 @@
import { Button } from "antd";
import ipcTypes from "../../../../util/ipcTypes.json";
const Home: React.FC = () => {
return (
<div>
<h1>Home</h1>
<Button
onClick={(): void => {
window.electron.ipcRenderer.send(
ipcTypes.toMain.debug.decodeEstimate
);
}}
>
Test Decode Estimate
</Button>
</div>
);
};
export default Home;

View File

@@ -16,7 +16,7 @@ const SettingsWatchedPaths: React.FC = () => {
});
}, []);
const handleAddPath = () => {
const handleAddPath = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.filepaths.add)
.then((paths: string[]) => {
@@ -24,7 +24,7 @@ const SettingsWatchedPaths: React.FC = () => {
});
};
const handleRemovePath = (path: string) => {
const handleRemovePath = (path: string): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.filepaths.remove, path)
.then((paths: string[]) => {

View File

@@ -0,0 +1,32 @@
/**
* Deep renames all keys in an object to lowercase
* @param obj - The object to transform
* @returns A new object with all keys converted to lowercase
*/
function deepLowerCaseKeys<T = any>(obj: any): T {
if (!obj || typeof obj !== "object") {
return obj;
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.map((item) => deepLowerCaseKeys(item)) as unknown as T;
}
// Handle objects
return Object.keys(obj).reduce(
(result, key) => {
const value = obj[key];
const lowercaseKey = key.toLowerCase();
result[lowercaseKey] =
typeof value === "object" && value !== null
? deepLowerCaseKeys(value)
: value;
return result;
},
{} as Record<string, any>
) as T;
}
export default deepLowerCaseKeys;

View File

@@ -2,6 +2,9 @@
"toMain": {
"test": "toMain_test",
"authStateChanged": "toMain_authStateChanged",
"debug": {
"decodeEstimate": "toMain_debug_decodeEstimate"
},
"watcher": {
"start": "toMain_watcher_start",
"stop": "toMain_watcher_stop"