Strip out partner related functionality.

This commit is contained in:
Patrick Fic
2025-12-08 13:53:15 -08:00
parent 267ef714a7
commit 39d81bbc6a
54 changed files with 79 additions and 3889 deletions

View File

@@ -1,14 +0,0 @@
VITE_COMPANY=ROME
# Fire Base Config
VITE_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_FIREBASE_CONFIG_TEST={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
# GraphQL Config
VITE_GRAPHQL_ENDPOINT=https://db.romeonline.io/v1/graphql
VITE_GRAPHQL_ENDPOINT_TEST=https://db.test.romeonline.io/v1/graphql
# Front End URL
VITE_FE_URL=https://romeonline.io
VITE_FE_URL_TEST=https://test.romeonline.io
# API Url
VITE_API_URL="https://api.romeonline.io"
VITE_API_TEST_URL="https://test.api.romeonline.io"

View File

@@ -1,18 +1,6 @@
# Shop Partner
An electron app that is replacing the existing Bodyshop Partner that was a C#/WPF Application.
# ESDP
The purpose of this application is to:
* Parse EMS files, and upload them to the IO back end.
* Receive requests for EMS file parsing
The following functionality will be coming:
* Interact with QuickBooks desktop (Windows Only)
* Paint scale integrations
* Parts Price Changes for CCC
Toggling between the Production and Test servers can be done by pressing `CTRL/CMD + SHIFT + T`, and then going to the application menu, and enabling test. The application will restart automatically.
## Dev and Build Notes
Unlike the main app, the dev mode will only connect to ImEX Online test data.
Building the app will require specifying the company to build for. Those details are captured in their respective ENV and YAML files.
# Outstanding Todos
* Update certificates and signing.
* Create S3 upload buckets
* Create S3 EMS upload bucket.

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.convenientbrands.bodyshop-desktop.keepalive</string>
<key>ProgramArguments</key>
<array>
<string>Shop Partner Keep Alive</string>
<string>imexmedia://keep-alive</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>${KEEP_ALIVE_INTERVAL_SECONDS}</integer>
</dict>
<dict>
<key>Label</key>
<string>com.imex.esdp.keepalive</string>
<key>ProgramArguments</key>
<array>
<string>Shop Partner Keep Alive</string>
<string>imexmedia://keep-alive</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>${KEEP_ALIVE_INTERVAL_SECONDS}</integer>
</dict>
</plist>

View File

@@ -1,3 +1,3 @@
provider: s3
bucket: imex-partner
bucket: esdp
region: ca-central-1

View File

@@ -1,61 +0,0 @@
appId: com.convenientbrands.bodyshop-desktop-rome
copyright: Convenient Brands, LLC.
productName: Rome Shop Partner
generateUpdatesFilesForAllChannels: true
directories:
buildResources: build
files:
- "!**/.vscode/*"
- "!**/.idea/*"
- "!src/*"
- "!electron.vite.config.{js,ts,mjs,cjs}"
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
- "!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}"
asarUnpack:
- resources/**
win:
executableName: ShopPartner
icon: resources/ro-icon.png
azureSignOptions:
endpoint: https://eus.codesigning.azure.net
certificateProfileName: ImEXRPS
codeSigningAccountName: ImEX
publisherName: ImEX Systems Inc.
nsis:
artifactName: rome-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
include: "scripts/installer.nsh" # Reference NSIS script from scripts directory
mac:
entitlementsInherit: build/entitlements.mac.plist
category: public.app-category.business
icon: resources/ro-icon.png
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
- CFBundleURLTypes:
- CFBundleTypeRole: Viewer # More specific role for protocol handling
CFBundleURLName: com.convenientbrands.bodyshop-desktop-rome
CFBundleURLSchemes:
- imexmedia
target:
- target: default
arch:
- arm64
- target: default
arch:
- x64
dmg:
artifactName: rome-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
appImage:
artifactName: rome-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
npmRebuild: false
publish:
provider: s3
bucket: rome-partner
region: us-east-2

View File

@@ -1,6 +1,6 @@
appId: com.convenientbrands.bodyshop-desktop-imex
copyright: Convenient Brands, LLC.
productName: ImEX Shop Partner
appId: com.imex.esdp
copyright: ImEX Systems Inc.
productName: EMS Uploader
generateUpdatesFilesForAllChannels: true
directories:
@@ -16,7 +16,7 @@ files:
asarUnpack:
- resources/**
win:
executableName: ShopPartner
executableName: EMSUploader
icon: resources/icon.png
azureSignOptions:
endpoint: https://eus.codesigning.azure.net
@@ -24,7 +24,7 @@ win:
codeSigningAccountName: ImEX
publisherName: ImEX Systems Inc.
nsis:
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
artifactName: esdp-${env.ARTIFACT_SUFFIX}${arch}.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
@@ -39,9 +39,9 @@ mac:
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
- CFBundleURLTypes:
- CFBundleTypeRole: Viewer # More specific role for protocol handling
CFBundleURLName: com.convenientbrands.bodyshop-desktop-imex
CFBundleURLName: com.imex.esdp
CFBundleURLSchemes:
- imexmedia
- esdp
target:
- target: default
arch:
@@ -50,11 +50,11 @@ mac:
arch:
- x64
dmg:
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
artifactName: esdp-${env.ARTIFACT_SUFFIX}${arch}.${ext}
appImage:
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
artifactName: esdp-${env.ARTIFACT_SUFFIX}${arch}.${ext}
npmRebuild: false
publish:
provider: s3
bucket: imex-partner
bucket: esdp
region: ca-central-1

View File

@@ -11,12 +11,12 @@ export default defineConfig({
}),
sentryVitePlugin({
org: "imex",
project: "imex-partner",
project: "esdp",
sourcemaps: {
filesToDeleteAfterUpload: ["**.js.map"],
},
release: {
name: `bodyshop-desktop@${process.env.npm_package_version}`,
name: `esdp@${process.env.npm_package_version}`,
},
}),
],

View File

@@ -1,10 +1,10 @@
{
"name": "bodyshop-desktop",
"version": "1.0.8",
"description": "Shop Management System Partner",
"name": "esdp",
"version": "0.0.1",
"description": "EMS Uploader",
"main": "./out/main/index.js",
"author": "Convenient Brands, LLC",
"homepage": "https://convenient-brands.com",
"author": "ImEX Systems Inc.",
"homepage": "https://imexsystems.ca",
"scripts": {
"format": "prettier --write .",
"lint": "eslint --cache .",
@@ -13,17 +13,13 @@
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build:imex": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --config electron-builder.imex.yml",
"build:rome": "node deploy/set-artifact-name.js electron-vite build --mode rome && node deploy/set-artifact-name.js electron-builder --config electron-builder.rome.yml",
"build:imex:publish": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --config electron-builder.imex.yml --publish always",
"build:rome:publish": "node deploy/set-artifact-name.js electron-vite build --mode rome && node deploy/set-artifact-name.js electron-builder --config electron-builder.rome.yml --publish always",
"build:imex:linux": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --config electron-builder.imex.yml --linux",
"build:rome:linux": "node deploy/set-artifact-name.js electron-vite build --mode rome && node deploy/set-artifact-name.js electron-builder --config electron-builder.rome.yml --linux",
"build": "node deploy/set-artifact-name.js electron-vite build && node deploy/set-artifact-name.js electron-builder ",
"build:publish": "node deploy/set-artifact-name.js electron-vite build && node deploy/set-artifact-name.js electron-builder --publish always",
"build:linux": "node deploy/set-artifact-name.js electron-vite build && node deploy/set-artifact-name.js electron-builder --linux",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --dir",
"build:win": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --win",
"build:mac": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --mac",
"build:linux": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --linux"
"build:mac": "node deploy/set-artifact-name.js electron-vite build --mode imex && node deploy/set-artifact-name.js electron-builder --mac"
},
"dependencies": {
"@apollo/client": "^3.13.6",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -1,158 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { ad1FieldLineDescriptors } from "../util/ems-interface/fielddescriptors/ad1-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateAd1File = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
INS_CO_ID: partsOrder.job.ins_co_nm,
INS_CO_NM: partsOrder.job.ins_co_nm,
INS_ADDR1: partsOrder.job.ins_addr1,
INS_ADDR2: partsOrder.job.ins_addr2,
INS_CITY: partsOrder.job.ins_city,
INS_ST: partsOrder.job.ins_st,
INS_ZIP: partsOrder.job.ins_zip,
INS_CTRY: partsOrder.job.ins_ctry,
INS_PH1: partsOrder.job.ins_ph1,
INS_PH1X: partsOrder.job.ins_ph1x,
INS_PH2: partsOrder.job.ins_ph2,
INS_PH2X: partsOrder.job.ins_ph2x,
INS_FAX: partsOrder.job.ins_fax,
INS_FAXX: partsOrder.job.ins_faxx,
INS_CT_LN: partsOrder.job.ins_ct_ln,
INS_CT_FN: partsOrder.job.ins_ct_fn,
INS_TITLE: partsOrder.job.ins_title,
INS_CT_PH: partsOrder.job.ins_ct_ph,
INS_CT_PHX: partsOrder.job.ins_ct_phx,
INS_EA: partsOrder.job.ins_ea,
INS_MEMO: partsOrder.job.ins_memo,
POLICY_NO: partsOrder.job.policy_no,
DED_AMT: partsOrder.job.ded_amt,
DED_STATUS: partsOrder.job.ded_status,
ASGN_NO: partsOrder.job.asgn_no,
ASGN_DATE: partsOrder.job.asgn_date
? new Date(partsOrder.job.asgn_date)
: null,
ASGN_TYPE: partsOrder.job.asgn_type,
CLM_NO: partsOrder.job.clm_no,
CLM_OFC_ID: partsOrder.job.clm_ofc_id,
CLM_OFC_NM: partsOrder.job.clm_ofc_nm,
CLM_ADDR1: partsOrder.job.clm_addr1,
CLM_ADDR2: partsOrder.job.clm_addr2,
CLM_CITY: partsOrder.job.clm_city,
CLM_ST: partsOrder.job.clm_st,
CLM_ZIP: partsOrder.job.clm_zip,
CLM_CTRY: partsOrder.job.clm_ctry,
CLM_PH1: partsOrder.job.clm_ph1,
CLM_PH1X: partsOrder.job.clm_ph1x,
CLM_PH2: partsOrder.job.clm_ph2,
CLM_PH2X: partsOrder.job.clm_ph2x,
CLM_FAX: partsOrder.job.clm_fax,
CLM_FAXX: partsOrder.job.clm_faxx,
CLM_CT_LN: partsOrder.job.clm_ct_ln,
CLM_CT_FN: partsOrder.job.clm_ct_fn,
CLM_TITLE: partsOrder.job.clm_title,
CLM_CT_PH: partsOrder.job.clm_ct_ph,
CLM_CT_PHX: partsOrder.job.clm_ct_phx,
CLM_EA: partsOrder.job.clm_ea,
PAYEE_NMS: partsOrder.job.payee_nms,
PAY_TYPE: partsOrder.job.pay_type,
PAY_DATE: partsOrder.job.pay_date,
PAY_CHKNM: null, // Explicitly set to null as in original code
PAY_AMT: null, // Explicitly set to null as in original code
PAY_MEMO: partsOrder.job.pay_memo,
AGT_CO_ID: partsOrder.job.agt_co_id,
AGT_CO_NM: partsOrder.job.agt_co_nm,
AGT_ADDR1: partsOrder.job.agt_addr1,
AGT_ADDR2: partsOrder.job.agt_addr2,
AGT_CITY: partsOrder.job.agt_city,
AGT_ST: partsOrder.job.agt_st,
AGT_ZIP: partsOrder.job.agt_zip,
AGT_CTRY: partsOrder.job.agt_ctry,
AGT_PH1: partsOrder.job.agt_ph1,
AGT_PH1X: partsOrder.job.agt_ph1x,
AGT_PH2: partsOrder.job.agt_ph2,
AGT_PH2X: partsOrder.job.agt_ph2x,
AGT_FAX: partsOrder.job.agt_fax,
AGT_FAXX: partsOrder.job.agt_faxx,
AGT_CT_LN: partsOrder.job.agt_ct_ln,
AGT_CT_FN: partsOrder.job.agt_ct_fn,
AGT_CT_PH: partsOrder.job.agt_ct_ph,
AGT_CT_PHX: partsOrder.job.agt_ct_phx,
AGT_EA: partsOrder.job.agt_ea,
AGT_LIC_NO: partsOrder.job.agt_lic_no,
LOSS_DATE: partsOrder.job.loss_date
? new Date(partsOrder.job.loss_date)
: null,
LOSS_CAT: null, // Explicitly set to null as in original code
LOSS_TYPE: null, // Explicitly set to null as in original code
LOSS_DESC: partsOrder.job.loss_desc,
THEFT_IND: null, // Explicitly set to null as in original code
CAT_NO: partsOrder.job.cat_no,
TLOS_IND: null, // Explicitly set to null as in original code
LOSS_MEMO: partsOrder.job.loss_memo,
CUST_PR: partsOrder.job.cust_pr,
INSD_LN: partsOrder.job.insd_ln,
INSD_FN: partsOrder.job.insd_fn,
INSD_TITLE: partsOrder.job.insd_title,
INSD_CO_NM: partsOrder.job.insd_co_nm,
INSD_ADDR1: partsOrder.job.insd_addr1,
INSD_ADDR2: partsOrder.job.insd_addr2,
INSD_CITY: partsOrder.job.insd_city,
INSD_ST: partsOrder.job.insd_st,
INSD_ZIP: partsOrder.job.insd_zip,
INSD_CTRY: partsOrder.job.insd_ctry,
INSD_PH1: partsOrder.job.insd_ph1,
INSD_PH1X: partsOrder.job.insd_ph1x,
INSD_PH2: partsOrder.job.insd_ph2,
INSD_PH2X: partsOrder.job.insd_ph2x,
INSD_FAX: partsOrder.job.insd_fax,
INSD_FAXX: partsOrder.job.insd_faxx,
INSD_EA: partsOrder.job.insd_ea,
OWNR_LN: partsOrder.job.ownr_ln,
OWNR_FN: partsOrder.job.ownr_fn,
OWNR_TITLE: partsOrder.job.ownr_title,
OWNR_CO_NM: partsOrder.job.ownr_co_nm,
OWNR_ADDR1: partsOrder.job.ownr_addr1,
OWNR_ADDR2: partsOrder.job.ownr_addr2,
OWNR_CITY: partsOrder.job.ownr_city,
OWNR_ST: partsOrder.job.ownr_st,
OWNR_ZIP: partsOrder.job.ownr_zip,
OWNR_CTRY: partsOrder.job.ownr_ctry,
OWNR_PH1: partsOrder.job.ownr_ph1,
OWNR_PH1X: partsOrder.job.ownr_ph1x,
OWNR_PH2: partsOrder.job.ownr_ph2,
OWNR_PH2X: partsOrder.job.ownr_ph2x,
OWNR_FAX: partsOrder.job.ownr_fax,
OWNR_FAXX: partsOrder.job.ownr_faxx,
OWNR_EA: partsOrder.job.ownr_ea,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD1`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD1`),
ad1FieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} AD1 file records added.`);
return true;
} catch (error) {
console.error("Error generating AD1 file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateAd1File;

View File

@@ -1,67 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { ad2FieldLineDescriptors } from "../util/ems-interface/fielddescriptors/ad2-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateAd2File = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
EST_CO_NM: partsOrder.job.est_co_nm,
EST_ADDR1: partsOrder.job.est_addr1,
EST_ADDR2: partsOrder.job.est_addr2,
EST_CITY: partsOrder.job.est_city,
EST_ST: partsOrder.job.est_st,
EST_ZIP: partsOrder.job.est_zip,
EST_CTRY: partsOrder.job.est_ctry,
EST_PH1: partsOrder.job.est_ph1,
EST_CT_LN: partsOrder.job.est_ct_ln,
EST_CT_FN: partsOrder.job.est_ct_fn,
EST_EA: partsOrder.job.est_ea,
CLMT_ADDR1: partsOrder.job.clm_addr1,
CLMT_ADDR2: partsOrder.job.clm_addr2,
CLMT_CITY: partsOrder.job.clm_city,
CLMT_ST: partsOrder.job.clm_st,
CLMT_ZIP: partsOrder.job.clm_zip,
CLMT_CTRY: partsOrder.job.clm_ctry,
CLMT_PH1: partsOrder.job.clm_ph1,
CLMT_PH1X: partsOrder.job.clm_ph1x,
CLMT_PH2: partsOrder.job.clm_ph2,
CLMT_PH2X: partsOrder.job.clm_ph2x,
CLMT_FAX: partsOrder.job.clm_fax,
CLMT_FAXX: partsOrder.job.clm_faxx,
CLMT_LN: partsOrder.job.clm_ct_ln,
CLMT_FN: partsOrder.job.clm_ct_fn,
CLMT_TITLE: partsOrder.job.clm_title,
CLMT_CT_PH: partsOrder.job.clm_ct_ph,
CLMT_CT_PHX: partsOrder.job.clm_ct_phx,
CLMT_EA: partsOrder.job.clm_ea,
RF_CO_NM: partsOrder.job.bodyshop.shopname,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD2`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD2`),
ad2FieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} AD2 file records added.`);
return true;
} catch (error) {
console.error("Error generating AD2 file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateAd2File;

View File

@@ -1,80 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { envFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/env-field-descriptor";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateEnvFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const dateNow = new Date();
const formatTime = (date: Date): string =>
`${date.getHours().toString().padStart(2, "0")}${date.getMinutes().toString().padStart(2, "0")}${date.getSeconds().toString().padStart(2, "0")}`;
const {
job: { ro_number, ciecaid },
} = partsOrder;
// Find the highest line_ind value
const lineInds = partsOrder.parts_order_lines.map(
(line) => line.jobline.line_ind,
);
const getNumber = (str: string): number => {
const match = str.match(/(\d+)$/);
return match ? parseInt(match[1], 10) : 0;
};
const highestLineInd = lineInds.reduce(
(max, current) => (getNumber(current) > getNumber(max) ? current : max),
lineInds[0] || "",
);
const records = [
{
EST_SYSTEM: "M",
SW_VERSION: "25.3",
DB_VERSION: "OCT_25_V",
DB_DATE: dateNow,
RO_ID: ro_number,
ESTFILE_ID: ciecaid,
SUPP_NO: highestLineInd ? getNumber(highestLineInd).toString() : "1",
EST_CTRY: "CAN",
TOP_SECRET: "00000000-0000-0000-0000-000000000000",
TRANS_TYPE: highestLineInd ? highestLineInd.charAt(0) : "S",
STATUS: false,
CREATE_DT: dateNow,
CREATE_TM: formatTime(dateNow),
TRANSMT_DT: dateNow,
TRANSMT_TM: formatTime(dateNow),
INCL_ADMIN: true,
INCL_VEH: true,
INCL_EST: true,
INCL_PROFL: false,
INCL_TOTAL: false,
INCL_VENDR: false,
EMS_VER: "2.0",
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.ENV`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.ENV`),
envFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} ENV file records added.`);
return true;
} catch (error) {
console.error("Error generating ENV file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateEnvFile;

View File

@@ -1,85 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
import { linFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/lin-field-descriptors";
const EmsPartsOrderGenerateLinFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = partsOrder.parts_order_lines.map((partsOrderLine) => ({
LINE_NO: partsOrderLine.jobline?.line_no,
LINE_IND: partsOrderLine.jobline?.line_ind,
LINE_REF: partsOrderLine.jobline?.line_ref,
TRAN_CODE: partsOrderLine.jobline?.tran_code ?? "1",
DB_REF: partsOrderLine.jobline?.db_ref,
UNQ_SEQ: partsOrderLine.jobline?.unq_seq,
PART_DES_J: false,
LINE_DESC: partsOrderLine.jobline?.line_desc,
PART_TYPE:
partsOrderLine.priceChange === true
? partsOrderLine.part_type
: partsOrderLine.jobline?.part_type,
GLASS_FLAG: partsOrderLine.jobline?.glass_flag,
OEM_PARTNO: partsOrderLine.jobline?.oem_partno,
PRICE_INC: partsOrderLine.jobline?.price_inc,
ALT_PART_I: partsOrderLine.jobline?.alt_part_i,
TAX_PART: partsOrderLine.jobline?.tax_part,
DB_PRICE: partsOrderLine.jobline?.db_price,
ACT_PRICE:
partsOrderLine.priceChange === true
? partsOrderLine.act_price
: partsOrderLine.jobline?.act_price,
PRICE_J: partsOrderLine.jobline?.price_j,
CERT_PART: partsOrderLine.jobline?.cert_part,
PART_QTY: partsOrderLine.jobline?.part_qty,
ALT_CO_ID: partsOrderLine.jobline?.alt_co_id,
ALT_PARTNO: partsOrderLine.jobline?.alt_partno,
ALT_OVERRD: partsOrderLine.jobline?.alt_overrd,
ALT_PARTM: partsOrderLine.jobline?.alt_partm,
PRT_DSMK_P: partsOrderLine.jobline?.prt_dsmk_p,
PRT_DSMK_M: partsOrderLine.jobline?.prt_dsmk_m,
MOD_LBR_TY: partsOrderLine.jobline?.mod_lbr_ty,
DB_HRS: partsOrderLine.jobline?.db_hrs,
MOD_LB_HRS: partsOrderLine.jobline?.mod_lb_hrs,
LBR_INC: partsOrderLine.jobline?.lbr_inc,
LBR_OP: partsOrderLine.jobline?.lbr_op,
LBR_HRS_J: partsOrderLine.jobline?.lbr_hrs_j,
LBR_TYP_J: partsOrderLine.jobline?.lbr_typ_j,
LBR_OP_J: partsOrderLine.jobline?.lbr_op_j,
PAINT_STG: partsOrderLine.jobline?.paint_stg,
PAINT_TONE: partsOrderLine.jobline?.paint_tone,
LBR_TAX: partsOrderLine.jobline?.lbr_tax,
LBR_AMT: partsOrderLine.jobline?.lbr_amt,
MISC_AMT: partsOrderLine.jobline?.misc_amt,
MISC_SUBLT: partsOrderLine.jobline?.misc_sublt,
MISC_TAX: partsOrderLine.jobline?.misc_tax,
BETT_TYPE: partsOrderLine.jobline?.bett_type,
BETT_PCTG: partsOrderLine.jobline?.bett_pctg,
BETT_AMT: partsOrderLine.jobline?.bett_amt,
BETT_TAX: partsOrderLine.jobline?.bett_tax,
}));
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.LIN`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.LIN`),
linFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} LIN file records added.`);
return true;
} catch (error) {
console.error("Error generating LIN file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateLinFile;

View File

@@ -1,59 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
import { pfhFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfh-field-descriptors";
const EmsPartsOrderGeneratePfhFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
ID_PRO_NAM: "REPAIR FACILITY", // Job.id_pro_nam?.Value
TAX_PRETHR: (partsOrder.job.tax_prethr || 0) * 100,
TAX_THRAMT: (partsOrder.job.tax_thramt || 0) * 100,
TAX_PSTTHR: (partsOrder.job.tax_pstthr || 0) * 100,
TAX_TOW_IN: true, // Job.tax_tow_in?.Value
TAX_TOW_RT: (partsOrder.job.tax_tow_rt || 0) * 100,
TAX_STR_IN: true, // Job.tax_str_in?.Value
TAX_STR_RT: (partsOrder.job.tax_str_rt || 0) * 100,
TAX_SUB_IN: true, // Job.tax_sub_in?.Value
TAX_SUB_RT: (partsOrder.job.tax_sub_rt || 0) * 100,
TAX_BTR_IN: true, // Job.tax_btr_in?.Value
TAX_LBR_RT:
(partsOrder.job.bodyshop?.bill_tax_rates?.state_tax_rate || 0) * 100,
TAX_GST_RT:
(partsOrder.job.bodyshop?.bill_tax_rates?.federal_tax_rate || 0) *
100,
TAX_GST_IN: true, // Job.tax_gst_in?.Value
ADJ_G_DISC: (partsOrder.job.adj_g_disc || 0) * 100,
ADJ_TOWDIS: (partsOrder.job.adj_towdis || 0) * 100,
ADJ_STRDIS: (partsOrder.job.adj_strdis || 0) * 100,
ADJ_BTR_IN: null, // Job.adj_btr_in?.Value
TAX_PREDIS: (partsOrder.job.tax_predis || 0) * 100,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFH`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFH`),
pfhFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFH file records added.`);
return true;
} catch (error) {
console.error("Error generating PFH file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfhFile;

View File

@@ -1,302 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPflLine } from "../decoder/decode-pfl.interface";
import { pflFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfl-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
import _ from "lodash";
const EmsPartsOrderGeneratePflFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
let records;
if (partsOrder.job.cieca_pfl && !_.isEmpty(partsOrder.job.cieca_pfl)) {
records = Object.keys(partsOrder.job.cieca_pfl).map((key) => {
const record: DecodedPflLine = partsOrder.job.cieca_pfl[key];
return uppercaseObjectKeys(record);
});
} else {
//We don't have the PFL data for an old job, so make it manually.
records = [
{
LBR_TYPE: "LAA",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_laa,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAB",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lab,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAD",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lad,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAE",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lae,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAF",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_laf,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAG",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lag,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAM",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lam,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAR",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lar,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAS",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_las,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAU",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lau,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA1",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la1,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA2",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la2,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA3",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la3,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA4",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la4,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
];
}
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFL`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFL`),
pflFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFL file records added.`);
return true;
} catch (error) {
console.error("Error generating PFL file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePflFile;

View File

@@ -1,105 +0,0 @@
import { DBFFile } from "dbffile";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfmLine } from "../decoder/decode-pfm.interface";
import { pfmFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfm-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePfmFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
let records;
if (partsOrder.job.materials && !_.isEmpty(partsOrder.job.materials)) {
records = Object.keys(partsOrder.job.materials).map((key) => {
const record: DecodedPfmLine = partsOrder.job.materials[key];
return uppercaseObjectKeys(record);
});
} else {
//Older records may not have materials, especially for ImEX.
records = [
{
MATL_TYPE: "MAPA",
CAL_CODE: null,
CAL_DESC: null,
CAL_MAXDLR: 0,
CAL_PRIP: 0,
CAL_SECP: 0,
MAT_CALP: 0,
CAL_PRETHR: 0,
CAL_PSTTHR: 0,
CAL_THRAMT: 0,
CAL_LBRMIN: 0,
CAL_LBRMAX: 0,
CAL_LBRRTE: partsOrder.job.rate_mapa,
CAL_OPCODE: null,
TAX_IND: true,
MAT_TAXP: null,
MAT_ADJP: null,
MAT_TX_TY1: null,
MAT_TX_IN1: null,
MAT_TX_TY2: null,
MAT_TX_IN2: null,
MAT_TX_TY3: null,
MAT_TX_IN3: null,
MAT_TX_TY4: null,
MAT_TX_IN4: null,
MAT_TX_TY5: null,
MAT_TX_IN5: null,
},
{
MATL_TYPE: "MASH",
CAL_CODE: null,
CAL_DESC: null,
CAL_MAXDLR: 0,
CAL_PRIP: 0,
CAL_SECP: 0,
MAT_CALP: 0,
CAL_PRETHR: 0,
CAL_PSTTHR: 0,
CAL_THRAMT: 0,
CAL_LBRMIN: 0,
CAL_LBRMAX: 0,
CAL_LBRRTE: partsOrder.job.rate_mash,
CAL_OPCODE: null,
TAX_IND: true,
MAT_TAXP: null,
MAT_ADJP: null,
MAT_TX_TY1: null,
MAT_TX_IN1: null,
MAT_TX_TY2: null,
MAT_TX_IN2: null,
MAT_TX_TY3: null,
MAT_TX_IN3: null,
MAT_TX_TY4: null,
MAT_TX_IN4: null,
MAT_TX_TY5: null,
MAT_TX_IN5: null,
},
];
}
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFM`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFM`),
pfmFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFM file records added.`);
return true;
} catch (error) {
console.error("Error generating PFM file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfmFile;

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { pfoFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfo-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePfoFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = []; //This was kept blank previously as well.
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFO`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFO`),
pfoFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFO file records added.`);
return true;
} catch (error) {
console.error("Error generating PFO file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfoFile;

View File

@@ -1,39 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfpLine } from "../decoder/decode-pfp.interface";
import { pfpFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfp-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePfpFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = Object.keys(partsOrder.job.parts_tax_rates).map((key) => {
const record: DecodedPfpLine = partsOrder.job.parts_tax_rates[key];
return uppercaseObjectKeys(record);
});
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFP`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFP`),
pfpFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFP file records added.`);
return true;
} catch (error) {
console.error("Error generating PFP file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfpFile;

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { pftFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pft-field-descriptor";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePftFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = []; //Left blank intentionally as per previous code.
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFT`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFT`),
pftFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFT file records added.`);
return true;
} catch (error) {
console.error("Error generating PFT file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePftFile;

View File

@@ -1,40 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedStlLine } from "../decoder/decode-stl.interface";
import { stlFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/stl-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateStlFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
//TODO: Add CIECA STL to parts order.
const records = Object.keys(partsOrder.job.cieca_stl?.data).map((key) => {
const record: DecodedStlLine = partsOrder.job.cieca_stl.data[key];
return uppercaseObjectKeys(record);
});
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.STL`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.STL`),
stlFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} STL file records added.`);
return true;
} catch (error) {
console.error("Error generating STL file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateStlFile;

View File

@@ -1,36 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { ttlFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/ttl-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateTtlFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
//TODO: Add CIECA STL to parts order.
const records = uppercaseObjectKeys(partsOrder.job.cieca_ttl?.data);
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.TTL`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.TTL`),
ttlFieldLineDescriptors,
);
await dbf.appendRecords([records]);
console.log(`${records.length} TTL file records added.`);
return true;
} catch (error) {
console.error("Error generating TTL file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateTtlFile;

View File

@@ -1,65 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { vehFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/veh-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateVehFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
IMPACT_1: partsOrder.job.area_of_damage?.impact1 || null,
IMPACT_2: partsOrder.job.area_of_damage?.impact2 || null,
DMG_MEMO: null,
DB_V_CODE: "",
PLATE_NO: partsOrder.job.plate_no || null,
PLATE_ST: partsOrder.job.plate_st || null,
V_VIN: partsOrder.job.v_vin || null,
V_COND: "",
V_PROD_DT: "",
V_MODEL_YR: partsOrder.job.v_model_yr || null,
V_MAKECODE: "",
V_MAKEDESC: partsOrder.job.v_make_desc || null,
V_MODEL: partsOrder.job.v_model_desc || null,
V_TYPE: partsOrder.job.vehicle?.v_type || null,
V_BSTYLE: partsOrder.job.vehicle?.v_bstyle || null,
V_TRIMCODE: partsOrder.job.vehicle?.v_trimcode || null,
TRIM_COLOR: partsOrder.job.vehicle?.trim_color || null,
V_MLDGCODE: partsOrder.job.vehicle?.v_mldgcode || null,
V_ENGINE: partsOrder.job.vehicle?.v_engine || null,
V_MILEAGE: partsOrder.job.vehicle?.v_mileage || null,
V_OPTIONS: null,
V_COLOR: partsOrder.job.vehicle?.v_color || null,
V_TONE: Number(partsOrder.job.vehicle?.v_tone) || null,
V_STAGE: null,
PAINT_CD1: partsOrder.job.vehicle?.v_paint_codes?.paint_cd1 || "",
PAINT_CD2: partsOrder.job.vehicle?.v_paint_codes?.paint_cd2 || "",
PAINT_CD3: partsOrder.job.vehicle?.v_paint_codes?.paint_cd3 || "",
V_MEMO: null,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.VEH`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.VEH`),
vehFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} VEH file records added.`);
return true;
} catch (error) {
console.error("Error generating VEH file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateVehFile;

View File

@@ -1,83 +0,0 @@
import log from "electron-log/main";
import express from "express";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import createdDirectoryIfNotExist from "../util/createDirectoryIfNotExist";
import EmsPartsOrderGenerateAd1File from "./ems-parts-order-generate-ad1";
import EmsPartsOrderGenerateAd2File from "./ems-parts-order-generate-ad2";
import EmsPartsOrderGenerateEnvFile from "./ems-parts-order-generate-env";
import EmsPartsOrderGenerateLinFile from "./ems-parts-order-generate-lin";
import EmsPartsOrderGeneratePfhFile from "./ems-parts-order-generate-pfh";
import EmsPartsOrderGeneratePflFile from "./ems-parts-order-generate-pfl";
import EmsPartsOrderGeneratePfmFile from "./ems-parts-order-generate-pfm";
import EmsPartsOrderGeneratePfoFile from "./ems-parts-order-generate-pfo";
import EmsPartsOrderGeneratePfpFile from "./ems-parts-order-generate-pfp";
import EmsPartsOrderGeneratePftFile from "./ems-parts-order-generate-pft";
import EmsPartsOrderGenerateStlFile from "./ems-parts-order-generate-stl";
import EmsPartsOrderGenerateTtlFile from "./ems-parts-order-generate-ttl";
import EmsPartsOrderGenerateVehFile from "./ems-parts-order-generate-veh";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const handleEMSPartsOrder = async (
req: express.Request,
res: express.Response,
): Promise<void> => {
//Route handler here only.
const partsOrderBody = req.body as EmsPartsOrder;
try {
await generateEMSPartsOrder(partsOrderBody);
res.status(200).json({ success: true });
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
res.status(500).json({
success: false,
error: "Error generating parts price change.",
...errorTypeCheck(error),
});
}
return;
};
const generateEMSPartsOrder = async (
partsOrder: EmsPartsOrder,
): Promise<void> => {
log.debug(" Generating parts price change");
//Check to make sure that the EMS Output file path exists. If it doesn't, create it. If it's not set, abandon ship.
const emsOutFilePath: string | null = store.get("settings.emsOutFilePath");
if (_.isEmpty(emsOutFilePath) || emsOutFilePath === null) {
log.error("EMS Out file path is not set");
throw new Error("EMS Out file path is not set");
}
try {
createdDirectoryIfNotExist(emsOutFilePath);
//Generate all required files: ad1, ad2, veh, lin, pfh, pfl, pfm,pfo, pfp, pft, stl, ttl
await EmsPartsOrderGenerateAd1File(partsOrder);
await EmsPartsOrderGenerateAd2File(partsOrder);
await EmsPartsOrderGenerateVehFile(partsOrder);
await EmsPartsOrderGenerateLinFile(partsOrder);
await EmsPartsOrderGeneratePfhFile(partsOrder);
await EmsPartsOrderGeneratePflFile(partsOrder);
await EmsPartsOrderGeneratePfmFile(partsOrder);
await EmsPartsOrderGeneratePfoFile(partsOrder);
await EmsPartsOrderGeneratePfpFile(partsOrder);
await EmsPartsOrderGeneratePftFile(partsOrder);
await EmsPartsOrderGenerateStlFile(partsOrder);
await EmsPartsOrderGenerateTtlFile(partsOrder);
await EmsPartsOrderGenerateEnvFile(partsOrder);
log.info(
"EMS Parts Order files generated successfully for " +
partsOrder.job.ciecaid,
);
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
throw error;
}
};
export { handleEMSPartsOrder };

View File

@@ -1,322 +0,0 @@
import { CiecaPfl } from "../decoder/decode-pfl.interface";
import { DecodedPfmLine } from "../decoder/decode-pfm.interface";
import { DecodedPfpLine } from "../decoder/decode-pfp.interface";
import { DecodedStlLine } from "../decoder/decode-stl.interface";
import { DecodedTtlLine } from "../decoder/decode-ttl.interface";
export interface TaxRate {
prt_type: string;
prt_discp: number;
prt_mktyp: boolean;
prt_mkupp: number;
prt_tax_in: boolean;
prt_tax_rt: number;
}
export interface BillTaxRates {
local_tax_rate: number;
state_tax_rate: number;
federal_tax_rate: number;
}
export interface PaintCodes {
paint_cd1: string | null;
paint_cd2: string | null;
paint_cd3: string | null;
}
export interface AreaOfDamage {
impact1: string;
impact2: string | null;
}
// Jobline export interface
export interface Jobline {
tran_code: string;
act_price: number;
db_ref: string;
db_price: number;
db_hrs: number;
glass_flag: boolean;
id: string;
lbr_amt: number;
lbr_hrs_j: boolean;
lbr_inc: boolean;
lbr_op: string;
lbr_op_j: boolean;
lbr_tax: boolean;
lbr_typ_j: boolean;
line_desc: string;
line_ind: string;
line_no: number;
line_ref: number;
location: string | null;
misc_amt: number;
misc_sublt: boolean;
misc_tax: boolean;
mod_lb_hrs: number;
mod_lbr_ty: string;
oem_partno: string;
op_code_desc: string;
paint_stg: number;
paint_tone: number;
part_qty: number;
part_type: string;
price_inc: boolean;
price_j: boolean;
prt_dsmk_m: number;
prt_dsmk_p: number;
tax_part: boolean;
unq_seq: number;
alt_co_id: string | null;
alt_overrd: boolean;
alt_part_i: boolean;
alt_partm: string | null;
alt_partno: string | null;
bett_amt: number;
bett_pctg: number;
bett_tax: boolean;
bett_type: string | null;
cert_part: boolean;
est_seq: string | null;
part_descj: boolean;
}
// Parts Order Line export interface
export interface PartsOrderLine {
jobline: Jobline;
act_price: number;
id: string;
db_price: number;
line_desc: string;
quantity: number;
part_type: string;
priceChange: boolean;
}
// Vehicle export interface
export interface Vehicle {
v_bstyle: string;
v_type: string;
v_trimcode: string | null;
v_tone: string;
v_stage: string;
v_prod_dt: string | null;
v_options: string | null;
v_paint_codes: PaintCodes;
v_model_yr: string;
v_model_desc: string;
v_mldgcode: string | null;
v_makecode: string;
v_make_desc: string;
v_engine: string;
v_cond: string;
v_color: string | null;
trim_color: string | null;
shopid: string;
plate_no: string;
plate_st: string;
db_v_code: string;
v_vin: string;
}
// Bodyshop export interface
export interface Bodyshop {
shopname: string;
bill_tax_rates: BillTaxRates;
}
// Job export interface
export interface Job {
bodyshop: Bodyshop;
ro_number: string;
clm_no: string;
asgn_no: string;
asgn_date: string;
state_tax_rate: number | null;
area_of_damage: AreaOfDamage;
asgn_type: string | null;
ciecaid: string;
cieca_pfl: CiecaPfl;
clm_addr1: string | null;
clm_city: string | null;
clm_addr2: string | null;
clm_ct_fn: string | null;
clm_ct_ln: string | null;
clm_ct_ph: string | null;
clm_ct_phx: string | null;
clm_ctry: string | null;
clm_ea: string | null;
clm_fax: string | null;
clm_faxx: string | null;
clm_ofc_id: string | null;
clm_ofc_nm: string | null;
clm_ph1: string | null;
clm_ph1x: string | null;
clm_ph2: string | null;
clm_ph2x: string | null;
clm_st: string | null;
clm_title: string | null;
clm_total: number;
clm_zip: string | null;
ded_amt: number;
est_addr1: string | null;
est_addr2: string | null;
est_city: string | null;
est_co_nm: string | null;
est_ct_fn: string;
est_ctry: string | null;
est_ct_ln: string;
est_ea: string;
est_ph1: string | null;
est_st: string | null;
est_zip: string | null;
g_bett_amt: number;
id: string;
ins_addr1: string | null;
ins_city: string | null;
ins_addr2: string | null;
ins_co_id: string | null;
ins_co_nm: string;
ins_ct_fn: string | null;
ins_ct_ln: string | null;
ins_ct_ph: string | null;
ins_ct_phx: string | null;
ins_ctry: string | null;
ins_ea: string | null;
ins_fax: string | null;
ins_faxx: string | null;
ins_memo: string | null;
ins_ph1: string | null;
ins_ph1x: string | null;
ins_ph2: string | null;
ins_ph2x: string | null;
ins_st: string | null;
ins_title: string | null;
ins_zip: string | null;
insd_addr1: string;
insd_addr2: string | null;
insd_city: string;
insd_co_nm: string | null;
insd_ctry: string | null;
insd_ea: string | null;
insd_fax: string | null;
insd_faxx: string | null;
insd_fn: string;
insd_ln: string;
insd_ph1: string;
insd_ph1x: string | null;
insd_ph2: string;
insd_ph2x: string | null;
insd_st: string;
insd_title: string | null;
insd_zip: string;
loss_cat: string;
loss_date: string;
loss_desc: string;
loss_of_use: string | null;
loss_type: string;
ownr_addr1: string;
ownr_addr2: string | null;
ownr_city: string;
ownr_co_nm: string | null;
ownr_ctry: string | null;
ownr_ea: string | null;
ownr_fax: string | null;
ownr_faxx: string | null;
ownr_ph1: string;
ownr_fn: string;
ownr_ln: string;
ownr_ph1x: string | null;
ownr_ph2: string;
ownr_ph2x: string | null;
ownr_st: string;
ownr_title: string | null;
ownr_zip: string;
parts_tax_rates: Record<string, DecodedPfpLine>;
pay_amt: number;
pay_date: string | null;
pay_type: string | null;
pay_chknm: string;
payee_nms: string | null;
plate_no: string;
plate_st: string;
po_number: string | null;
policy_no: string;
tax_lbr_rt: number;
tax_levies_rt: number;
tax_paint_mat_rt: number;
tax_predis: number;
tax_prethr: number;
tax_pstthr: number;
tax_registration_number: string | null;
tax_str_rt: number;
tax_shop_mat_rt: number;
tax_sub_rt: number;
tax_thramt: number;
tax_tow_rt: number;
theft_ind: boolean;
tlos_ind: boolean;
towin: boolean;
v_color: string | null;
v_make_desc: string;
v_model_desc: string;
v_model_yr: string;
v_vin: string;
vehicle: Vehicle;
agt_zip: string | null;
agt_st: string | null;
agt_ph2x: string | null;
agt_ph2: string | null;
agt_ph1x: string | null;
agt_ph1: string | null;
agt_lic_no: string | null;
agt_faxx: string | null;
agt_fax: string | null;
agt_ea: string | null;
agt_ctry: string | null;
agt_ct_phx: string | null;
agt_ct_ph: string | null;
agt_ct_ln: string | null;
agt_ct_fn: string | null;
agt_co_nm: string | null;
agt_co_id: string | null;
agt_city: string | null;
agt_addr1: string | null;
agt_addr2: string | null;
adj_g_disc: number;
rate_matd: number | null;
rate_mash: number;
rate_mapa: number;
rate_mahw: number;
rate_macs: number;
rate_mabl: number | null;
rate_ma3s: number;
rate_ma2t: number;
rate_ma2s: number;
rate_lau: number;
rate_las: number;
rate_lar: number;
rate_lam: number;
rate_lag: number;
rate_laf: number;
rate_lae: number | null;
rate_lad: number | null;
rate_lab: number;
rate_laa: number;
rate_la4: number;
rate_la3: number;
rate_la2: number;
rate_la1: number;
materials: Record<string, DecodedPfmLine>;
cieca_stl: {
data: Array<DecodedStlLine>;
};
cieca_ttl: { data: DecodedTtlLine };
}
// Main Parts Order export interface
export interface EmsPartsOrder {
parts_order_lines: PartsOrderLine[];
job: Job;
}

View File

@@ -1,210 +0,0 @@
import cors from "cors";
import { app } from "electron";
import log from "electron-log/main";
import express from "express";
import http from "http";
import errorTypeCheck from "../../util/errorTypeCheck";
import ImportJob from "../decoder/decoder";
import folderScan from "../decoder/folder-scan";
import { handleEMSPartsOrder } from "../ems-parts-order/ems-parts-order-handler";
import { handleShopMetaDataFetch } from "../ipc/ipcMainHandler.user";
import { handlePartsPriceChangeRequest } from "../ppc/ppc-handler";
import { handleQuickBookRequest } from "../quickbooks-desktop/quickbooks-desktop";
export default class LocalServer {
private readonly app: express.Application;
private server: http.Server | null;
private PORT = 1337;
constructor() {
this.server = null;
this.app = express();
this.configureMiddleware();
this.configureRoutes();
}
private configureMiddleware(): void {
const allowedOrigins = [
"http://localhost",
"https://localhost",
"http://localhost:3000",
"https://localhost:3000",
"https://test.imex.online",
"https://imex.online",
"https://test.romeonline.io",
"https://romeonline.io",
"https://www.test.imex.online",
"https://www.imex.online",
"https://www.test.romeonline.io",
"https://www.romeonline.io",
];
this.app.use(
cors({
origin: (origin, callback) => {
// Allow requests with no origin (like mobile apps, curl requests)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
return callback(null, true);
} else {
return callback(null, false);
}
},
credentials: true,
}),
);
// Parse JSON bodies
this.app.use(express.json());
this.app.use(express.urlencoded());
//Add logger Middleware
this.app.use((req, res, next) => {
const startTime = Date.now();
const requestId = Math.random().toString(36).substring(2, 15);
// Log request details
log.info(
`[HTTP Server] [${requestId}] Request: ${req.method} ${req.url}`,
);
log.info(
`[HTTP Server] [${requestId}] Headers: ${JSON.stringify(req.headers)}`,
);
// Log request body if it exists
if (req.body && Object.keys(req.body).length > 0) {
log.info(
`[HTTP Server] [${requestId}] Body: ${JSON.stringify(req.body)}`,
);
}
// Capture the original methods
const originalSend = res.send;
const originalJson = res.json;
// Override send method to log response
res.send = function (body): express.Response {
log.info(`[HTTP Server] [${requestId}] Response body: ${body}`);
log.info(
`[HTTP Server] [${requestId}] Response time: ${Date.now() - startTime}ms`,
);
return originalSend.call(this, body);
};
// Override json method to log response
res.json = function (body): express.Response {
log.info(
`[HTTP Server] [${requestId}] Response body: ${JSON.stringify(body)}`,
);
log.info(
`[HTTP Server] [${requestId}] Response time: ${Date.now() - startTime}ms`,
);
return originalJson.call(this, body);
};
next();
});
}
private configureRoutes(): void {
// Basic health check endpoint
this.app.get("/health", (_req: express.Request, res: express.Response) => {
res.status(200).json({ status: "ok" });
});
this.app.post("/ping", (_req, res) => {
res.status(200).json({
appVer: app.getVersion(),
qbPath: app.getPath("userData"), //TODO: Resolve to actual QB file path.
});
});
this.app.post("/qb", handleQuickBookRequest);
this.app.post("/scan", async (_req, res): Promise<void> => {
log.debug("[HTTP Server] Scan request received");
const files = await folderScan();
res.status(200).json(files);
return;
});
this.app.post("/ppc", handlePartsPriceChangeRequest);
this.app.post("/oec", handleEMSPartsOrder);
this.app.post(
"/import",
async (req: express.Request, res: express.Response) => {
log.debug("[HTTP Server] Import request received");
const { filepath } = req.body;
if (!filepath) {
res.status(400).json({ error: "filepath is required" });
return;
}
try {
await ImportJob(filepath);
res.status(200).json({ success: true });
} catch (error) {
log.error(
"[HTTP Server] Error importing file",
errorTypeCheck(error),
);
res.status(500).json({
success: false,
error: "Error importing file",
...errorTypeCheck(error),
});
}
},
);
this.app.post(
"/refresh",
async (_req: express.Request, res: express.Response) => {
log.debug("[HTTP Server] Refresh request received");
try {
await handleShopMetaDataFetch(true);
res.status(200).json({ success: true });
} catch (error) {
log.error(
"[HTTP Server] Error refreshing shop metadata",
errorTypeCheck(error),
);
res.status(500).json({
success: false,
error: "Error importing file",
...errorTypeCheck(error),
});
}
},
);
// Add more routes as needed
}
public start(): void {
try {
this.server = http.createServer(this.app);
this.server.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
log.error(
`[HTTP Server] Port ${this.PORT} is already in use. Please use a different port.`,
);
} else {
log.error(`[HTTP Server] Server error: ${error.message}`);
}
});
this.server.listen(this.PORT, () => {
log.info(
`[HTTP Server] Local HTTP server running on port ${this.PORT}`,
);
});
} catch (error: unknown) {
log.error("[HTTP Server] Error starting server", errorTypeCheck(error));
}
}
public stop(): void {
if (this.server) {
this.server.close();
log.info("[HTTP Server] Local HTTP server stopped");
}
}
}

View File

@@ -14,7 +14,6 @@ import log from "electron-log/main";
import { autoUpdater } from "electron-updater";
import path, { join } from "path";
import imexAppIcon from "../../resources/icon.png?asset";
import romeAppIcon from "../../resources/ro-icon.png?asset";
import {
default as ErrorTypeCheck,
@@ -22,11 +21,8 @@ import {
} from "../util/errorTypeCheck";
import ipcTypes from "../util/ipcTypes.json";
import ImportJob from "./decoder/decoder";
import LocalServer from "./http-server/http-server";
import store from "./store/store";
import { checkForAppUpdates } from "./util/checkForAppUpdates";
import { getMainWindow } from "./util/toRenderer";
import { GetAllEnvFiles } from "./watcher/watcher";
import { dumpMemoryStatsToFile } from "../util/memUsage";
import {
isKeepAliveAgentInstalled,
setupKeepAliveAgent,
@@ -35,11 +31,13 @@ import {
isKeepAliveTaskInstalled,
setupKeepAliveTask,
} from "./setup-keep-alive-task";
import store from "./store/store";
import { checkForAppUpdates } from "./util/checkForAppUpdates";
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
import ongoingMemoryDump, { dumpMemoryStatsToFile } from "../util/memUsage";
import { getMainWindow } from "./util/toRenderer";
import { GetAllEnvFiles } from "./watcher/watcher";
const appIconToUse =
import.meta.env.VITE_COMPANY === "IMEX" ? imexAppIcon : romeAppIcon;
const appIconToUse = imexAppIcon;
Sentry.init({
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
@@ -54,11 +52,10 @@ log.transports.console.format =
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
log.transports.file.maxSize = 50 * 1024 * 1024; // 50 MB
const isMac: boolean = process.platform === "darwin";
const protocol: string = "imexmedia";
const protocol: string = "esdp";
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
let isKeepAliveLaunch = false; // Track if launched via keep-alive
// Initialize the server
const localServer = new LocalServer();
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
@@ -95,7 +92,7 @@ function createWindow(): void {
icon: appIconToUse,
}
: {}),
title: "Shop Partner",
title: "EMS Uploader",
webPreferences: {
preload: join(__dirname, "../preload/index.js"),
sandbox: false,
@@ -425,13 +422,6 @@ function createWindow(): void {
if (!isKeepAliveLaunch) {
mainWindow.show(); // Show only if not a keep-alive launch
}
//Start the HTTP server.
// Start the local HTTP server
try {
localServer.start();
} catch (error) {
log.error("Failed to start HTTP server:", errorTypeCheck(error));
}
});
mainWindow.on("close", (event: Electron.Event) => {
@@ -476,7 +466,7 @@ app.whenReady().then(async () => {
log.debug("App is ready, initializing shortcuts and protocol handlers.");
if (platform.isWindows) {
app.setAppUserModelId("Shop Partner");
app.setAppUserModelId("esdp");
}
app.on("browser-window-created", (_, window) => {
@@ -508,17 +498,8 @@ app.whenReady().then(async () => {
//Dynamically load ipcMain handlers once ready.
try {
const { initializeCronTasks } = await import("./ipc/ipcMainConfig");
await import("./ipc/ipcMainConfig");
log.debug("Successfully loaded ipcMainConfig");
try {
await initializeCronTasks();
log.info("Cron tasks initialized successfully");
} catch (error) {
log.warn("Non-fatal: Failed to initialize cron tasks", {
...ErrorTypeCheck(error),
});
}
} catch (error) {
log.error("Fatal: Failed to load ipcMainConfig", {
...ErrorTypeCheck(error),
@@ -599,7 +580,6 @@ app.whenReady().then(async () => {
//The update itself will run when the bodyshop record is queried to know what release channel to use.
openMainWindow();
ongoingMemoryDump();
app.on("activate", function () {
openMainWindow();
@@ -661,7 +641,6 @@ ipcMain.on(ipcTypes.toMain.updates.apply, () => {
});
function preQuitMethods(): void {
localServer.stop();
const currentSetting = store.get("app.openOnStartup") as boolean;
if (!import.meta.env.DEV) {
app.setLoginItemSettings({

View File

@@ -27,27 +27,6 @@ import {
ipcMainHandleAuthStateChanged,
ipMainHandleResetPassword,
} from "./ipcMainHandler.user";
import cron from "node-cron";
import { PaintScaleConfig, PaintScaleType } from "../../util/types/paintScale";
import { ppgInputHandler, ppgOutputHandler } from "./paintScaleHandlers/PPG";
const initializeCronTasks = async () => {
try {
// Fetch input and output configurations
const inputConfigs = await SettingsPaintScaleInputConfigsGet();
const outputConfigs = await SettingsPaintScaleOutputConfigsGet();
// Start input cron tasks
await handlePaintScaleInputCron(inputConfigs);
log.info("Initialized input cron tasks on app startup");
// Start output cron tasks
await handlePaintScaleOutputCron(outputConfigs);
log.info("Initialized output cron tasks on app startup");
} catch (error) {
log.error("Error initializing cron tasks on app startup:", error);
}
};
// Log all IPC messages and their payloads
const logIpcMessages = (): void => {
@@ -71,75 +50,6 @@ const logIpcMessages = (): void => {
});
};
// Input handler map
const inputTypeHandlers: Partial<
Record<PaintScaleType, (config: PaintScaleConfig) => Promise<void>>
> = {
[PaintScaleType.PPG]: ppgInputHandler,
// Add other input type handlers as needed
};
// Output handler map
const outputTypeHandlers: Partial<
Record<PaintScaleType, (config: PaintScaleConfig) => Promise<void>>
> = {
[PaintScaleType.PPG]: ppgOutputHandler,
// Add other output type handlers as needed
};
// Default handler for unsupported types
const defaultHandler = async (config: PaintScaleConfig) => {
log.debug(
`No handler defined for type ${config.type} in config ${config.id}`,
);
};
// Input cron job management
let inputCronTasks: { [id: string]: cron.ScheduledTask } = {};
const handlePaintScaleInputCron = async (configs: PaintScaleConfig[]) => {
Object.values(inputCronTasks).forEach((task) => task.stop());
inputCronTasks = {};
const validConfigs = configs.filter(
(config) => config.path && config.path.trim() !== "",
);
validConfigs.forEach((config) => {
const cronExpression = `*/${config.pollingInterval} * * * *`;
inputCronTasks[config.id] = cron.schedule(cronExpression, async () => {
const handler = inputTypeHandlers[config.type] || defaultHandler;
await handler(config);
});
log.info(
`Started input cron task for config ${config.id} (type: ${config.type}) with interval ${config.pollingInterval}m`,
);
});
};
// Output cron job management
let outputCronTasks: { [id: string]: cron.ScheduledTask } = {};
const handlePaintScaleOutputCron = async (configs: PaintScaleConfig[]) => {
Object.values(outputCronTasks).forEach((task) => task.stop());
outputCronTasks = {};
const validConfigs = configs.filter(
(config) => config.path && config.path.trim() !== "",
);
validConfigs.forEach((config) => {
const cronExpression = `*/${config.pollingInterval} * * * *`;
outputCronTasks[config.id] = cron.schedule(cronExpression, async () => {
const handler = outputTypeHandlers[config.type] || defaultHandler;
await handler(config);
});
log.info(
`Started output cron task for config ${config.id} (type: ${config.type}) with interval ${config.pollingInterval}m`,
);
});
};
// Existing IPC handlers...
ipcMain.on(ipcTypes.toMain.test, () =>
@@ -230,25 +140,6 @@ ipcMain.handle(
SettingsPaintScaleOutputPathSet,
);
// IPC handlers for updating paint scale cron
ipcMain.on(
ipcTypes.toMain.settings.paintScale.updateInputCron,
(_event, configs: PaintScaleConfig[]) => {
handlePaintScaleInputCron(configs).catch((error) => {
log.error(`Error handling paint scale input cron for configs: ${error}`);
});
},
);
ipcMain.on(
ipcTypes.toMain.settings.paintScale.updateOutputCron,
(_event, configs: PaintScaleConfig[]) => {
handlePaintScaleOutputCron(configs).catch((error) => {
log.error(`Error handling paint scale output cron for configs: ${error}`);
});
},
);
ipcMain.handle(ipcTypes.toMain.user.getActiveShop, () => {
return store.get("app.bodyshop.shopname");
});
@@ -273,6 +164,4 @@ ipcMain.on(ipcTypes.toMain.updates.download, () => {
});
});
export { initializeCronTasks };
logIpcMessages();

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import { envFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/env-field-descriptor";
import { deleteEmsFileIfExists, generatePpcFilePath } from "../util/ems-util";
import { PpcJob } from "./ppc-handler";
const GenerateEnvFile = async (job: PpcJob): Promise<boolean> => {
const records = [
{
EST_SYSTEM: "C",
RO_ID: job.ro_number,
ESTFILE_ID: job.ciecaid,
STATUS: false,
INCL_ADMIN: true,
INCL_VEH: true,
INCL_EST: true,
INCL_PROFL: true,
INCL_TOTAL: true,
INCL_VENDR: false,
},
];
await deleteEmsFileIfExists(generatePpcFilePath(`${job.ciecaid}.ENV`));
const dbf = await DBFFile.create(
generatePpcFilePath(`${job.ciecaid}.ENV`),
envFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} LIN file records added.`);
return true;
};
export default GenerateEnvFile;

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import { linFieldDescriptors } from "../util/ems-interface/fielddescriptors/lin-field-descriptor";
import { deleteEmsFileIfExists, generatePpcFilePath } from "../util/ems-util";
import { PpcJob } from "./ppc-handler";
import errorTypeCheck from "../../util/errorTypeCheck";
const GenerateLinFile = async (job: PpcJob): Promise<boolean> => {
try {
const records = job.joblines.map((line) => {
return {
//TODO: There are missing types here. May require server side updates, but we are missing things like LINE_NO, LINE_IND, etc.
TRAN_CODE: "2",
UNQ_SEQ: line.unq_seq,
ACT_PRICE: line.act_price,
};
});
await deleteEmsFileIfExists(generatePpcFilePath(`${job.ciecaid}.LIN`));
const dbf = await DBFFile.create(
generatePpcFilePath(`${job.ciecaid}.LIN`),
linFieldDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} LIN file records added.`);
return true;
} catch (error) {
console.error("Error generating PPC LIN file", errorTypeCheck(error));
throw error;
}
};
export default GenerateLinFile;

View File

@@ -1,67 +0,0 @@
import { UUID } from "crypto";
import log from "electron-log/main";
import express from "express";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import createdDirectoryIfNotExist from "../util/createDirectoryIfNotExist";
import GenerateEnvFile from "./ppc-generate-env";
import GenerateLinFile from "./ppc-generate-lin";
const handlePartsPriceChangeRequest = async (
req: express.Request,
res: express.Response,
): Promise<void> => {
//Route handler here only.
const job = req.body as PpcJob;
try {
await generatePartsPriceChange(job);
res.status(200).json({ success: true });
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
res.status(500).json({
success: false,
error: "Error generating parts price change.",
...errorTypeCheck(error),
});
}
return;
};
const generatePartsPriceChange = async (job: PpcJob): Promise<void> => {
log.debug(" Generating parts price change");
//Check to make sure that the PPC Output file path exists. If it doesn't, create it. If it's not set, abandon ship.
const ppcOutFilePath: string | null = store.get("settings.ppcFilePath");
if (_.isEmpty(ppcOutFilePath) || ppcOutFilePath === null) {
log.error("PPC file path is not set");
throw new Error("PPC file path is not set");
}
try {
createdDirectoryIfNotExist(ppcOutFilePath);
await GenerateLinFile(job);
await GenerateEnvFile(job);
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
throw error;
}
};
export interface PpcJob {
id: UUID;
ciecaid: string;
ro_number: string;
joblines: {
removed: boolean;
act_price_before_ppc: number | null;
id: string;
act_price: number;
unq_seq: string; //TODO: Might be a number.
}[];
bodyshop: {
timezone: string;
};
}
export { handlePartsPriceChangeRequest };

View File

@@ -1,30 +0,0 @@
using System;
using Interop.QBFC16; // Ensure this matches your DLL version
public class QuickBooksConnector
{
public string ProcessQBXML(string qbxmlRequest)
{
try
{
QBSessionManager sessionManager = new QBSessionManager();
sessionManager.OpenConnection("", "YourAppName");
sessionManager.BeginSession("", ENOpenMode.omDontCare);
IMsgSetRequest requestMsgSet = sessionManager.CreateMsgSetRequest("US", 13, 0);
requestMsgSet.AppendXML(qbxmlRequest);
IMsgSetResponse responseMsgSet = sessionManager.DoRequests(requestMsgSet);
string qbxmlResponse = responseMsgSet.ToXMLString();
sessionManager.EndSession();
sessionManager.CloseConnection();
return qbxmlResponse;
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}

View File

@@ -1,130 +0,0 @@
import log from "electron-log/main";
import { UUID } from "crypto";
import { Request, Response } from "express";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let Winax: any; // Declare Winax as any to avoid TypeScript errors on non-Windows platforms
if (process.platform === "win32") {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Winax = require("winax");
}
export async function handleQuickBookRequest(
req: Request,
res: Response,
): Promise<void> {
if (process.platform !== "win32") {
res.status(500).json({
error: "QuickBooks Desktop integration is only available on Windows",
});
return;
}
const QbFilePath: string = `C:\\Users\\PatrickFic\\Development\\FRODO COLLISION.QBW`;
// ||
// (store.get("settings.qbFilePath") as string) F
if (_.isEmpty(QbFilePath)) {
res.status(400).json({ error: "Quickbooks file path not set" });
return;
}
const qbxmlRequestList = req.body as Array<{
id: UUID;
okStatusCodes: Array<string>;
qbxml: string;
}>;
const returnResponse: Array<{
Id: UUID;
Success: boolean;
ErrorMessage: string;
}> = [];
//Connect to the QuickBooks File
let requestProcessor;
try {
requestProcessor = new Winax.Object("QBXMLRP2.RequestProcessor.2");
requestProcessor.OpenConnection(QbFilePath, "ShopPartnerActualRequest");
} catch (error) {
log.error(
"Error instnatiating QuickBooks Request Processor",
QbFilePath,
errorTypeCheck(error),
);
res.status(500).json({ error: "Error connecting to QuickBooks" });
return;
}
const ticket = requestProcessor.BeginSession(QbFilePath, 2); //2 indicated qbFileOpenModeDoNotCare
log.info("Quickbooks Ticket", ticket);
for (const qbxmlRequest of qbxmlRequestList) {
try {
//TODO: Refactor to not create a new connection every time.
const QuickBooksResponse = requestProcessor.ProcessRequest(
ticket,
qbxmlRequest.qbxml,
);
log.info("QuickBooks Raw Response: ", QuickBooksResponse);
returnResponse.push({
Id: qbxmlRequest.id,
Success:
QuickBooksResponse.StatusCode === "0" ||
qbxmlRequest.okStatusCodes.includes(QuickBooksResponse.StatusCode),
ErrorMessage: QuickBooksResponse,
});
} catch (error) {
log.error(
"Error running transaction",
ticket,
qbxmlRequest,
errorTypeCheck(error),
);
}
}
requestProcessor.EndSession(ticket);
requestProcessor.CloseConnection();
res.json(qbxmlRequestList);
}
//This set of functions works.
export function TestQB(): void {
if (process.platform !== "win32") {
log.warn("TestQB is only available on Windows");
return;
}
let requestProcessor, ticket;
try {
requestProcessor = new Winax.Object("QBXMLRP.RequestProcessor.1");
requestProcessor.OpenConnection("", "ShopPartnerOneoFf");
ticket = requestProcessor.BeginSession("", 2); //2 indicated qbFileOOpenModeDoNotCare
requestProcessor.ProcessRequest(
ticket,
`<?qbxml version="16.0"?>
<QBXML>
<QBXMLMsgsRq onError="stopOnError">
<AccountQueryRq requestID="1"> </AccountQueryRq>
</QBXMLMsgsRq>
</QBXML>`,
);
} catch (error) {
log.error(
"Error instnatiating QuickBooks Request Processor",
errorTypeCheck(error),
);
return;
}
log.log("Ticket", ticket);
requestProcessor.EndSession(ticket);
requestProcessor.CloseConnection();
return;
}

View File

@@ -5,11 +5,7 @@ const store = new Store({
settings: {
runOnStartup: true,
filepaths: [],
ppcFilePath: null,
emsOutFilePath: null,
qbFilePath: "",
runWatcherOnStartup: true,
enableMemDebug: false,
polling: {
enabled: false,
interval: 30000,
@@ -25,9 +21,6 @@ const store = new Store({
user: null,
isTest: false,
bodyshop: {},
masterdata: {
opcodes: null,
},
},
},
});

View File

@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>Shop Partner</title>
<title>EMS Uploader</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta
http-equiv="Content-Security-Policy"

View File

@@ -1,97 +0,0 @@
import { test, expect } from "@playwright/test";
import { Page } from "@playwright/test";
// src/renderer/src/App.test.tsx
// Mock data
const mockUser = {
uid: "test123",
email: "test@example.com",
displayName: "Test User",
toJSON: () => ({
uid: "test123",
email: "test@example.com",
displayName: "Test User",
}),
};
test.describe("App Component", () => {
let page: Page;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
// Mock Firebase Auth
await page.addInitScript(() => {
window.mockAuthState = null;
// Mock the firebase auth module
jest.mock("./util/firebase", () => ({
auth: {
onAuthStateChanged: (callback) => {
callback(window.mockAuthState);
// Return mock unsubscribe function
return () => {};
},
},
}));
// Mock electron IPC
window.electron = {
ipcRenderer: {
send: jest.fn(),
},
};
});
await page.goto("/");
});
test("should show SignInForm when user is not authenticated", async () => {
await page.evaluate(() => {
window.mockAuthState = null;
});
await page.reload();
// Check if SignInForm is visible
const signInForm = await page
.locator("form")
.filter({ hasText: "Sign In" });
await expect(signInForm).toBeVisible();
});
test("should show routes when user is authenticated", async () => {
await page.evaluate((user) => {
window.mockAuthState = user;
}, mockUser);
await page.reload();
// Check if AuthHome is visible
const authHome = await page.locator('div:text("AuthHome")');
await expect(authHome).toBeVisible();
// Check that electron IPC was called with auth state
await expect(
page.evaluate(() => {
return window.electron.ipcRenderer.send.mock.calls.length > 0;
}),
).resolves.toBe(true);
});
test("should navigate to settings page when authenticated", async () => {
await page.evaluate((user) => {
window.mockAuthState = user;
}, mockUser);
await page.reload();
// Navigate to settings
await page.click('a[href="/settings"]');
// Check if Settings page is visible
const settingsPage = await page.locator('div:text("Settings")');
await expect(settingsPage).toBeVisible();
});
});

View File

@@ -1,44 +1,16 @@
import "@ant-design/v5-patch-for-react-19";
import { Layout, Skeleton, ConfigProvider, Badge } from "antd";
import { User } from "firebase/auth";
import { useEffect, useState, FC } from "react";
import { Badge, ConfigProvider, Layout, Skeleton } from "antd";
import { FC } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { Provider } from "react-redux";
import { HashRouter, Route, Routes } from "react-router";
import ipcTypes from "../../util/ipcTypes.json";
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
import Settings from "./components/Settings/Settings";
import SignInForm from "./components/SignInForm/SignInForm";
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
import reduxStore from "./redux/redux-store";
import { auth } from "./util/firebase";
import { NotificationProvider } from "./util/notificationContext";
const App: FC = () => {
const [user, setUser] = useState<User | boolean | null>(false);
useEffect(() => {
// Only set up the listener once when component mounts
if (auth.currentUser) {
setUser(auth.currentUser);
} else {
setUser(false);
}
const unsubscribe = auth.onAuthStateChanged((user: User | null) => {
setUser(user);
//Send back to the main process so that it knows we are authenticated.
if (user) {
window.electron.ipcRenderer.send(
ipcTypes.toMain.authStateChanged,
user.toJSON(),
);
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start);
}
});
// Clean up the listener when component unmounts
return (): void => unsubscribe();
}, []);
const isTest = window.api.isTest();
return (
@@ -58,23 +30,19 @@ const App: FC = () => {
<HashRouter>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<NotificationProvider>
<Skeleton loading={user === false} active>
<Skeleton loading={false} active>
<Layout style={{ minHeight: "100vh" }}>
{!user ? (
<SignInForm />
) : (
<Badge.Ribbon
text={isTest && "Connected to Test"}
color={isTest ? "red" : undefined}
>
<Layout.Content style={{ padding: "0 24px" }}>
<UpdateAvailable />
<Routes>
<Route path="/" element={<Settings />} />
</Routes>
</Layout.Content>
</Badge.Ribbon>
)}
<Badge.Ribbon
text={isTest && "Connected to Test"}
color={isTest ? "red" : undefined}
>
<Layout.Content style={{ padding: "0 24px" }}>
<UpdateAvailable />
<Routes>
<Route path="/" element={<Settings />} />
</Routes>
</Layout.Content>
</Badge.Ribbon>
</Layout>
</Skeleton>
</NotificationProvider>

View File

@@ -1,11 +0,0 @@
import { FC } from "react";
const Home: FC = () => {
return (
<div>
<h1>Home</h1>
</div>
);
};
export default Home;

View File

@@ -1,164 +0,0 @@
import { useState, useEffect } from "react";
import ipcTypes from "../../../../../util/ipcTypes.json";
import {
PaintScaleConfig,
PaintScaleType,
} from "../../../../../util/types/paintScale";
import { message } from "antd";
import { useTranslation } from "react-i18next";
type ConfigType = "input" | "output";
export const usePaintScaleConfig = (configType: ConfigType) => {
const [paintScaleConfigs, setPaintScaleConfigs] = useState<
PaintScaleConfig[]
>([]);
const { t } = useTranslation();
// Get the appropriate IPC methods based on config type
const getConfigsMethod =
configType === "input"
? ipcTypes.toMain.settings.paintScale.getInputConfigs
: ipcTypes.toMain.settings.paintScale.getOutputConfigs;
const setConfigsMethod =
configType === "input"
? ipcTypes.toMain.settings.paintScale.setInputConfigs
: ipcTypes.toMain.settings.paintScale.setOutputConfigs;
const setPathMethod =
configType === "input"
? ipcTypes.toMain.settings.paintScale.setInputPath
: ipcTypes.toMain.settings.paintScale.setOutputPath;
// Load paint scale configs on mount
useEffect(() => {
window.electron.ipcRenderer
.invoke(getConfigsMethod)
.then((configs: PaintScaleConfig[]) => {
// Ensure all configs have a pollingInterval and type (for backward compatibility)
const defaultPolling = configType === "input" ? 1440 : 60;
const updatedConfigs = configs.map((config) => ({
...config,
pollingInterval: config.pollingInterval || defaultPolling, // Default to 1440 for input, 60 for output
type: config.type || PaintScaleType.PPG, // Default type if missing
}));
setPaintScaleConfigs(updatedConfigs || []);
})
.catch((error) => {
console.error(
`Failed to load paint scale ${configType} configs:`,
error,
);
});
}, [getConfigsMethod]);
// Save configs to store and notify main process of config changes
const saveConfigs = (configs: PaintScaleConfig[]) => {
window.electron.ipcRenderer
.invoke(setConfigsMethod, configs)
.then(() => {
// Notify main process to update cron job
if (configType === "input") {
window.electron.ipcRenderer.send(
ipcTypes.toMain.settings.paintScale.updateInputCron,
configs,
);
} else if (configType === "output") {
window.electron.ipcRenderer.send(
ipcTypes.toMain.settings.paintScale.updateOutputCron,
configs,
);
}
})
.catch((error) => {
console.error(
`Failed to save paint scale ${configType} configs:`,
error,
);
});
};
// New helper to check if a path is unique across input and output configs
const checkPathUnique = async (newPath: string): Promise<boolean> => {
try {
const inputConfigs: PaintScaleConfig[] =
await window.electron.ipcRenderer.invoke(
ipcTypes.toMain.settings.paintScale.getInputConfigs,
);
const outputConfigs: PaintScaleConfig[] =
await window.electron.ipcRenderer.invoke(
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
);
const allConfigs = [...inputConfigs, ...outputConfigs];
// Allow updating the current config even if its current value equals newPath.
return !allConfigs.some((config) => config.path === newPath);
} catch (error) {
console.error("Failed to check unique path:", error);
return false;
}
};
// Handle adding a new paint scale config
const handleAddConfig = (type: PaintScaleType) => {
const defaultPolling = configType === "input" ? 1440 : 60;
const newConfig: PaintScaleConfig = {
id: Date.now().toString(),
type,
pollingInterval: defaultPolling, // Default to 1440 for input, 60 for output
};
const updatedConfigs = [...paintScaleConfigs, newConfig];
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
};
// Handle removing a config
const handleRemoveConfig = (id: string) => {
const updatedConfigs = paintScaleConfigs.filter(
(config) => config.id !== id,
);
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
};
// Handle path selection (modified to check directory uniqueness)
const handlePathChange = async (id: string) => {
try {
const path: string | null = await window.electron.ipcRenderer.invoke(
setPathMethod,
id,
);
if (path) {
const isUnique = await checkPathUnique(path);
if (!isUnique) {
message.error(t("settings.errors.duplicatePath"));
return;
}
const updatedConfigs = paintScaleConfigs.map((config) =>
config.id === id ? { ...config, path } : config,
);
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
}
} catch (error) {
console.error(`Failed to set paint scale ${configType} path:`, error);
}
};
// Handle polling interval change
const handlePollingIntervalChange = (id: string, pollingInterval: number) => {
const updatedConfigs = paintScaleConfigs.map((config) =>
config.id === id ? { ...config, pollingInterval } : config,
);
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
};
return {
paintScaleConfigs,
handleAddConfig,
handleRemoveConfig,
handlePathChange,
handlePollingIntervalChange,
};
};

View File

@@ -1,46 +0,0 @@
import { FolderOpenFilled } from "@ant-design/icons";
import { Button, Card, Input, Space } from "antd";
import { useEffect, useState, FC } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const SettingsEmsOutFilePath: FC = () => {
const { t } = useTranslation();
const [emsFilePath, setEmsFilePath] = useState<string | null>(null);
const getPollingStateFromStore = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.getEmsOutFilePath)
.then((filePath: string | null) => {
setEmsFilePath(filePath);
});
};
//Get state first time it renders.
useEffect(() => {
getPollingStateFromStore();
}, []);
const handlePathChange = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.setEmsOutFilePath)
.then((filePath: string | null) => {
setEmsFilePath(filePath);
});
};
return (
<Card title={t("settings.labels.emsOutFilePath")}>
<Space wrap>
<Input
value={emsFilePath || ""}
placeholder={t("settings.labels.emsOutFilePath")}
disabled
/>
<Button onClick={handlePathChange} icon={<FolderOpenFilled />} />
</Space>
</Card>
);
};
export default SettingsEmsOutFilePath;

View File

@@ -1,183 +0,0 @@
import {
CheckCircleFilled,
FileAddFilled,
FolderOpenFilled,
WarningFilled,
} from "@ant-design/icons";
import {
Button,
Card,
Input,
Modal,
Select,
Space,
Table,
Tag,
theme,
Tooltip,
} from "antd";
import { JSX, useState } from "react";
import { useTranslation } from "react-i18next";
import {
PaintScaleConfig,
PaintScaleType,
paintScaleTypeOptions,
} from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleInputPaths = (): JSX.Element => {
const { t } = useTranslation();
const { token } = theme.useToken(); // Access theme tokens
const {
paintScaleConfigs,
handleAddConfig,
handleRemoveConfig,
handlePathChange,
handlePollingIntervalChange,
} = usePaintScaleConfig("output");
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
// Show modal when adding a new path
const showAddPathModal = () => {
setSelectedType(null);
setIsModalVisible(true);
};
// Handle modal confirmation
const handleModalOk = () => {
if (selectedType) {
handleAddConfig(selectedType);
setIsModalVisible(false);
}
};
// Handle modal cancellation
const handleModalCancel = () => {
setIsModalVisible(false);
};
// Table columns for paint scale configs
const columns = [
{
title: t("settings.labels.paintScaleType"),
dataIndex: "type",
key: "type",
render: (type: PaintScaleType) => {
const typeOption = paintScaleTypeOptions.find(
(option) => option.value === type,
);
const label = typeOption ? typeOption.label : type;
const colorMap: Partial<Record<PaintScaleType, string>> = {
[PaintScaleType.PPG]: "blue",
// Add other types and colors as needed
};
return <Tag color={colorMap[type] || "default"}>{label}</Tag>;
},
},
{
title: t("settings.labels.paintScalePath"),
dataIndex: "path",
key: "path",
render: (path: string | null, record: PaintScaleConfig) => {
const isValid = path && path.trim() !== "";
return (
<Space>
<Input
value={path || ""}
placeholder={t("settings.labels.paintScalePath")}
disabled
style={{
borderColor: isValid ? token.colorSuccess : token.colorError, // Use semantic tokens
}}
suffix={
<Tooltip
title={
isValid
? t("settings.labels.validPath")
: t("settings.labels.invalidPath")
}
>
{isValid ? (
<CheckCircleFilled style={{ color: token.colorSuccess }} />
) : (
<WarningFilled style={{ color: token.colorError }} />
)}
</Tooltip>
}
/>
<Button
onClick={() => handlePathChange(record.id)}
icon={<FolderOpenFilled />}
/>
</Space>
);
},
},
{
title: t("settings.labels.pollingInterval"),
dataIndex: "pollingInterval",
key: "pollingInterval",
render: (pollingInterval: number, record: PaintScaleConfig) => (
<Input
type="number"
value={pollingInterval}
onChange={(e) =>
handlePollingIntervalChange(record.id, Number(e.target.value))
}
style={{ width: 100 }}
placeholder={t("settings.labels.pollingInterval")}
/>
),
},
{
title: t("settings.labels.actions"),
key: "actions",
render: (_: any, record: PaintScaleConfig) => (
<Button danger onClick={() => handleRemoveConfig(record.id)}>
{t("settings.labels.remove")}
</Button>
),
},
];
return (
<>
<Card
title={t("settings.labels.paintScaleSettingsInput")}
extra={
<Button onClick={showAddPathModal} icon={<FileAddFilled />}>
{t("settings.actions.addpath")}
</Button>
}
>
<Table
dataSource={paintScaleConfigs}
columns={columns}
rowKey="id"
pagination={false}
/>
</Card>
<Modal
title={t("settings.labels.selectPaintScaleType")}
open={isModalVisible}
onOk={handleModalOk}
onCancel={handleModalCancel}
okButtonProps={{ disabled: !selectedType }}
>
<Select
value={selectedType}
options={paintScaleTypeOptions}
onChange={(value) => setSelectedType(value)}
style={{ width: "100%" }}
placeholder={t("settings.labels.selectPaintScaleType")}
/>
</Modal>
</>
);
};
export default SettingsPaintScaleInputPaths;

View File

@@ -1,173 +0,0 @@
import {
CheckCircleFilled,
FileAddFilled,
FolderOpenFilled,
WarningFilled,
} from "@ant-design/icons";
import {
Button,
Card,
Input,
Modal,
Select,
Space,
Table,
Tag,
theme,
} from "antd";
import { JSX, useState } from "react";
import { useTranslation } from "react-i18next";
import {
PaintScaleConfig,
PaintScaleType,
paintScaleTypeOptions,
} from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleOutputPaths = (): JSX.Element => {
const { token } = theme.useToken();
const { t } = useTranslation();
const {
paintScaleConfigs,
handleAddConfig,
handleRemoveConfig,
handlePathChange,
handlePollingIntervalChange,
} = usePaintScaleConfig("input");
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
// Show modal when adding a new path
const showAddPathModal = () => {
setSelectedType(null);
setIsModalVisible(true);
};
// Handle modal confirmation
const handleModalOk = () => {
if (selectedType) {
handleAddConfig(selectedType);
setIsModalVisible(false);
}
};
// Handle modal cancellation
const handleModalCancel = () => {
setIsModalVisible(false);
};
// Table columns for paint scale configs
const columns = [
{
title: t("settings.labels.paintScaleType"),
dataIndex: "type",
key: "type",
render: (type: PaintScaleType) => {
const typeOption = paintScaleTypeOptions.find(
(option) => option.value === type,
);
const label = typeOption ? typeOption.label : type;
const colorMap: Partial<Record<PaintScaleType, string>> = {
[PaintScaleType.PPG]: "blue",
// Add other types and colors as needed
};
return <Tag color={colorMap[type] || "default"}>{label}</Tag>;
},
},
{
title: t("settings.labels.paintScalePath"),
dataIndex: "path",
key: "path",
render: (path: string | null, record: PaintScaleConfig) => {
const isValid = path && path.trim() !== "";
return (
<Space>
<Input
value={path || ""}
placeholder={t("settings.labels.paintScalePath")}
disabled
style={{
borderColor: isValid ? token.colorSuccess : token.colorError,
}}
suffix={
isValid ? (
<CheckCircleFilled style={{ color: token.colorSuccess }} />
) : (
<WarningFilled style={{ color: token.colorError }} />
)
}
/>
<Button
onClick={() => handlePathChange(record.id)}
icon={<FolderOpenFilled />}
/>
</Space>
);
},
},
{
title: t("settings.labels.pollingInterval"),
dataIndex: "pollingInterval",
key: "pollingInterval",
render: (pollingInterval: number, record: PaintScaleConfig) => (
<Input
type="number"
value={pollingInterval}
onChange={(e) =>
handlePollingIntervalChange(record.id, Number(e.target.value))
}
style={{ width: 100 }}
placeholder={t("settings.labels.pollingInterval")}
/>
),
},
{
title: t("settings.labels.actions"),
key: "actions",
render: (_: any, record: PaintScaleConfig) => (
<Button danger onClick={() => handleRemoveConfig(record.id)}>
{t("settings.labels.remove")}
</Button>
),
},
];
return (
<>
<Card
title={t("settings.labels.paintScaleSettingsOutput")}
extra={
<Button onClick={showAddPathModal} icon={<FileAddFilled />}>
{t("settings.actions.addpath")}
</Button>
}
>
<Table
dataSource={paintScaleConfigs}
columns={columns}
rowKey="id"
pagination={false}
/>
</Card>
<Modal
title={t("settings.labels.selectPaintScaleType")}
open={isModalVisible}
onOk={handleModalOk}
onCancel={handleModalCancel}
okButtonProps={{ disabled: !selectedType }}
>
<Select
value={selectedType}
options={paintScaleTypeOptions}
onChange={(value) => setSelectedType(value)}
style={{ width: "100%" }}
placeholder={t("settings.labels.selectPaintScaleType")}
/>
</Modal>
</>
);
};
export default SettingsPaintScaleOutputPaths;

View File

@@ -1,46 +0,0 @@
import { FolderOpenFilled } from "@ant-design/icons";
import { Button, Card, Input, Space } from "antd";
import { useEffect, useState, FC } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const SettingsPpcFilepath: FC = () => {
const { t } = useTranslation();
const [ppcFilePath, setPpcFilePath] = useState<string | null>(null);
const getPollingStateFromStore = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.getPpcFilePath)
.then((filePath: string | null) => {
setPpcFilePath(filePath);
});
};
//Get state first time it renders.
useEffect(() => {
getPollingStateFromStore();
}, []);
const handlePathChange = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.setPpcFilePath)
.then((filePath: string | null) => {
setPpcFilePath(filePath);
});
};
return (
<Card title={t("settings.labels.ppcfilepath")}>
<Space wrap>
<Input
value={ppcFilePath || ""}
placeholder={t("settings.labels.ppcfilepath")}
disabled
/>
<Button onClick={handlePathChange} icon={<FolderOpenFilled />} />
</Space>
</Card>
);
};
export default SettingsPpcFilepath;

View File

@@ -3,43 +3,23 @@ import { Col, Row } from "antd";
import { FC } from "react";
import SettingsWatchedPaths from "./Settings.WatchedPaths";
import SettingsWatcher from "./Settings.Watcher";
import Welcome from "../Welcome/Welcome";
import SettingsPpcFilepath from "./Settings.PpcFilePath";
import SettingsEmsOutFilePath from "./Settings.EmsOutFilePath";
import SettingsPaintScaleInputPaths from "./Settings.PaintScaleInputPaths";
import SettingsPaintScaleOutputPaths from "./Settings.PaintScaleOutputPaths";
const colSpans = {
md: 12, // Two columns on medium screens and above
sm: 24, // One column on small screens
md: 12, // Two columns on medium screens and above
sm: 24, // One column on small screens
};
const Settings: FC = () => {
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<Welcome />
</Col>
<Col {...colSpans}>
<SettingsWatchedPaths />
</Col>
<Col {...colSpans}>
<SettingsWatcher />
</Col>
<Col {...colSpans}>
<SettingsPpcFilepath />
</Col>
<Col {...colSpans}>
<SettingsEmsOutFilePath />
</Col>
<Col {...colSpans}>
<SettingsPaintScaleInputPaths />
</Col>
<Col {...colSpans}>
<SettingsPaintScaleOutputPaths />
</Col>
</Row>
);
return (
<Row gutter={[16, 16]}>
<Col {...colSpans}>
<SettingsWatchedPaths />
</Col>
<Col {...colSpans}>
<SettingsWatcher />
</Col>
</Row>
);
};
export default Settings;
export default Settings;

View File

@@ -1,142 +0,0 @@
import { auth } from "@renderer/util/firebase";
import type { FormProps } from "antd";
import { Alert, Button, Card, Form, Input, Typography } from "antd";
import log from "electron-log/renderer";
import { signInWithEmailAndPassword } from "firebase/auth";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import errorTypeCheck from "../../../../util/errorTypeCheck";
import ipcTypes from "../../../../util/ipcTypes.json";
const { Title } = Typography;
type FieldType = {
username: string;
password: string;
remember?: string;
};
const SignInForm: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
const onFinish: FormProps<FieldType>["onFinish"] = async (values) => {
const { username, password } = values;
setLoading(true);
try {
const result = await signInWithEmailAndPassword(auth, username, password);
log.debug("Login result", result);
} catch (error) {
log.error("Login error", errorTypeCheck(error));
setError(t("auth.login.error"));
} finally {
setLoading(false);
}
};
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
errorInfo,
) => {
log.log("Failed:", errorInfo);
};
return (
<Card
style={{
maxWidth: 600,
margin: "auto auto",
borderRadius: 8,
paddingLeft: 48,
paddingRight: 48,
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
}}
>
<div style={{ textAlign: "center", marginBottom: 24 }}>
<Title level={2}>
{import.meta.env.VITE_COMPANY === "IMEX"
? t("title.imex")
: t("title.rome")}
</Title>
</div>
<Form
name="desktop-sign-in"
layout="vertical"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
requiredMark={false}
>
{error && (
<Form.Item>
<Alert
message={error}
type="error"
showIcon
style={{ marginBottom: 16 }}
/>
</Form.Item>
)}
<Form.Item<FieldType>
label="Username"
name="username"
rules={[
{
required: true,
message: t(
"auth.login.usernameRequired",
"Please enter your username",
),
},
]}
>
<Input size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password"
name="password"
rules={[
{
required: true,
message: t(
"auth.login.passwordRequired",
"Please enter your password",
),
},
]}
>
<Input.Password size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
loading={loading}
htmlType="submit"
size="large"
block
>
{t("auth.login.login")}
</Button>
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: "center" }}>
<Button
type="link"
onClick={(): void => {
window.electron.ipcRenderer.send(
ipcTypes.toMain.user.resetPassword,
);
}}
>
{t("auth.login.resetpassword")}
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default SignInForm;

View File

@@ -1,15 +0,0 @@
import { JSX, useState } from "react";
function Versions(): JSX.Element {
const [versions] = useState(window.electron.process.versions);
return (
<ul className="versions">
<li className="electron-version">Electron v{versions.electron}</li>
<li className="chrome-version">Chromium v{versions.chrome}</li>
<li className="node-version">Node v{versions.node}</li>
</ul>
);
}
export default Versions;

View File

@@ -1,49 +0,0 @@
import { LogoutOutlined } from "@ant-design/icons";
import { auth } from "@renderer/util/firebase";
import { Button, Space, Typography } from "antd";
import { isEmpty } from "lodash";
import { JSX, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const Welcome = (): JSX.Element => {
const { t } = useTranslation();
const [shopName, setShopName] = useState<string | null>(null);
useEffect(() => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.user.getActiveShop)
.then((shopName: string) => {
console.log("Active shop name:", shopName);
setShopName(shopName);
});
}, []);
return (
<>
<Typography.Title level={4}>
{t("auth.labels.welcome", {
name: isEmpty(auth.currentUser?.displayName)
? auth.currentUser?.email
: `${auth.currentUser?.displayName} (${auth.currentUser?.email})`.trim(),
})}
</Typography.Title>
<Space align="baseline">
<Typography.Paragraph>{shopName || ""}</Typography.Paragraph>
<Button
size="small"
danger
icon={<LogoutOutlined />}
onClick={(): void => {
auth.signOut().catch((error) => {
console.error("Sign out error:", error);
});
}}
>
{t("navigation.signout")}
</Button>
</Space>
</>
);
};
export default Welcome;

View File

@@ -1,17 +0,0 @@
export enum PaintScaleType {
PPG = "PPG",
}
export interface PaintScaleConfig {
id: string;
path?: string;
type: PaintScaleType;
pollingInterval: number;
}
export const paintScaleTypeOptions = Object.values(PaintScaleType).map(
(type) => ({
value: type,
label: type,
}),
);

View File

@@ -1,489 +0,0 @@
import { test, expect, type Page } from "@playwright/test";
test.beforeEach(async ({ page }) => {
await page.goto("https://demo.playwright.dev/todomvc");
});
const TODO_ITEMS = [
"buy some cheese",
"feed the cat",
"book a doctors appointment",
] as const;
test.describe("New Todo", () => {
test("should allow me to add todo items", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press("Enter");
// Make sure the list only has one todo item.
await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]);
// Create 2nd todo.
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press("Enter");
// Make sure the list now has two todo items.
await expect(page.getByTestId("todo-title")).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[1],
]);
await checkNumberOfTodosInLocalStorage(page, 2);
});
test("should clear text input field when an item is added", async ({
page,
}) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create one todo item.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press("Enter");
// Check that input is empty.
await expect(newTodo).toBeEmpty();
await checkNumberOfTodosInLocalStorage(page, 1);
});
test("should append new items to the bottom of the list", async ({
page,
}) => {
// Create 3 items.
await createDefaultTodos(page);
// create a todo count locator
const todoCount = page.getByTestId("todo-count");
// Check test using different methods.
await expect(page.getByText("3 items left")).toBeVisible();
await expect(todoCount).toHaveText("3 items left");
await expect(todoCount).toContainText("3");
await expect(todoCount).toHaveText(/3/);
// Check all items in one call.
await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS);
await checkNumberOfTodosInLocalStorage(page, 3);
});
});
test.describe("Mark all as completed", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test.afterEach(async ({ page }) => {
await checkNumberOfTodosInLocalStorage(page, 3);
});
test("should allow me to mark all items as completed", async ({ page }) => {
// Complete all todos.
await page.getByLabel("Mark all as complete").check();
// Ensure all todos have 'completed' class.
await expect(page.getByTestId("todo-item")).toHaveClass([
"completed",
"completed",
"completed",
]);
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
});
test("should allow me to clear the complete state of all items", async ({
page,
}) => {
const toggleAll = page.getByLabel("Mark all as complete");
// Check and then immediately uncheck.
await toggleAll.check();
await toggleAll.uncheck();
// Should be no completed classes.
await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]);
});
test("complete all checkbox should update state when items are completed / cleared", async ({
page,
}) => {
const toggleAll = page.getByLabel("Mark all as complete");
await toggleAll.check();
await expect(toggleAll).toBeChecked();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Uncheck first todo.
const firstTodo = page.getByTestId("todo-item").nth(0);
await firstTodo.getByRole("checkbox").uncheck();
// Reuse toggleAll locator and make sure its not checked.
await expect(toggleAll).not.toBeChecked();
await firstTodo.getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Assert the toggle all is checked again.
await expect(toggleAll).toBeChecked();
});
});
test.describe("Item", () => {
test("should allow me to mark items as complete", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
// Check first item.
const firstTodo = page.getByTestId("todo-item").nth(0);
await firstTodo.getByRole("checkbox").check();
await expect(firstTodo).toHaveClass("completed");
// Check second item.
const secondTodo = page.getByTestId("todo-item").nth(1);
await expect(secondTodo).not.toHaveClass("completed");
await secondTodo.getByRole("checkbox").check();
// Assert completed class.
await expect(firstTodo).toHaveClass("completed");
await expect(secondTodo).toHaveClass("completed");
});
test("should allow me to un-mark items as complete", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
const firstTodo = page.getByTestId("todo-item").nth(0);
const secondTodo = page.getByTestId("todo-item").nth(1);
const firstTodoCheckbox = firstTodo.getByRole("checkbox");
await firstTodoCheckbox.check();
await expect(firstTodo).toHaveClass("completed");
await expect(secondTodo).not.toHaveClass("completed");
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await firstTodoCheckbox.uncheck();
await expect(firstTodo).not.toHaveClass("completed");
await expect(secondTodo).not.toHaveClass("completed");
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
});
test("should allow me to edit an item", async ({ page }) => {
await createDefaultTodos(page);
const todoItems = page.getByTestId("todo-item");
const secondTodo = todoItems.nth(1);
await secondTodo.dblclick();
await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue(
TODO_ITEMS[1],
);
await secondTodo
.getByRole("textbox", { name: "Edit" })
.fill("buy some sausages");
await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter");
// Explicitly assert the new text value.
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
"buy some sausages",
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, "buy some sausages");
});
});
test.describe("Editing", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test("should hide other controls when editing", async ({ page }) => {
const todoItem = page.getByTestId("todo-item").nth(1);
await todoItem.dblclick();
await expect(todoItem.getByRole("checkbox")).not.toBeVisible();
await expect(
todoItem.locator("label", {
hasText: TODO_ITEMS[1],
}),
).not.toBeVisible();
await checkNumberOfTodosInLocalStorage(page, 3);
});
test("should save edits on blur", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.fill("buy some sausages");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.dispatchEvent("blur");
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
"buy some sausages",
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, "buy some sausages");
});
test("should trim entered text", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.fill(" buy some sausages ");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.press("Enter");
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
"buy some sausages",
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, "buy some sausages");
});
test("should remove the item if an empty text string was entered", async ({
page,
}) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.press("Enter");
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test("should cancel edits on escape", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).dblclick();
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.fill("buy some sausages");
await todoItems
.nth(1)
.getByRole("textbox", { name: "Edit" })
.press("Escape");
await expect(todoItems).toHaveText(TODO_ITEMS);
});
});
test.describe("Counter", () => {
test("should display the current number of todo items", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
// create a todo count locator
const todoCount = page.getByTestId("todo-count");
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press("Enter");
await expect(todoCount).toContainText("1");
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press("Enter");
await expect(todoCount).toContainText("2");
await checkNumberOfTodosInLocalStorage(page, 2);
});
});
test.describe("Clear completed button", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
});
test("should display the correct text", async ({ page }) => {
await page.locator(".todo-list li .toggle").first().check();
await expect(
page.getByRole("button", { name: "Clear completed" }),
).toBeVisible();
});
test("should remove completed items when clicked", async ({ page }) => {
const todoItems = page.getByTestId("todo-item");
await todoItems.nth(1).getByRole("checkbox").check();
await page.getByRole("button", { name: "Clear completed" }).click();
await expect(todoItems).toHaveCount(2);
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test("should be hidden when there are no items that are completed", async ({
page,
}) => {
await page.locator(".todo-list li .toggle").first().check();
await page.getByRole("button", { name: "Clear completed" }).click();
await expect(
page.getByRole("button", { name: "Clear completed" }),
).toBeHidden();
});
});
test.describe("Persistence", () => {
test("should persist its data", async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
const todoItems = page.getByTestId("todo-item");
const firstTodoCheck = todoItems.nth(0).getByRole("checkbox");
await firstTodoCheck.check();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(["completed", ""]);
// Ensure there is 1 completed item.
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
// Now reload.
await page.reload();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(["completed", ""]);
});
});
test.describe("Routing", () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
// make sure the app had a chance to save updated todos in storage
// before navigating to a new view, otherwise the items can get lost :(
// in some frameworks like Durandal
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
});
test("should allow me to display active items", async ({ page }) => {
const todoItem = page.getByTestId("todo-item");
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole("link", { name: "Active" }).click();
await expect(todoItem).toHaveCount(2);
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test("should respect the back button", async ({ page }) => {
const todoItem = page.getByTestId("todo-item");
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await test.step("Showing all items", async () => {
await page.getByRole("link", { name: "All" }).click();
await expect(todoItem).toHaveCount(3);
});
await test.step("Showing active items", async () => {
await page.getByRole("link", { name: "Active" }).click();
});
await test.step("Showing completed items", async () => {
await page.getByRole("link", { name: "Completed" }).click();
});
await expect(todoItem).toHaveCount(1);
await page.goBack();
await expect(todoItem).toHaveCount(2);
await page.goBack();
await expect(todoItem).toHaveCount(3);
});
test("should allow me to display completed items", async ({ page }) => {
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole("link", { name: "Completed" }).click();
await expect(page.getByTestId("todo-item")).toHaveCount(1);
});
test("should allow me to display all items", async ({ page }) => {
await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole("link", { name: "Active" }).click();
await page.getByRole("link", { name: "Completed" }).click();
await page.getByRole("link", { name: "All" }).click();
await expect(page.getByTestId("todo-item")).toHaveCount(3);
});
test("should highlight the currently applied filter", async ({ page }) => {
await expect(page.getByRole("link", { name: "All" })).toHaveClass(
"selected",
);
//create locators for active and completed links
const activeLink = page.getByRole("link", { name: "Active" });
const completedLink = page.getByRole("link", { name: "Completed" });
await activeLink.click();
// Page change - active items.
await expect(activeLink).toHaveClass("selected");
await completedLink.click();
// Page change - completed items.
await expect(completedLink).toHaveClass("selected");
});
});
async function createDefaultTodos(page: Page) {
// create a new todo locator
const newTodo = page.getByPlaceholder("What needs to be done?");
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press("Enter");
}
}
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
return await page.waitForFunction((e) => {
return JSON.parse(localStorage["react-todos"]).length === e;
}, expected);
}
async function checkNumberOfCompletedTodosInLocalStorage(
page: Page,
expected: number,
) {
return await page.waitForFunction((e) => {
return (
JSON.parse(localStorage["react-todos"]).filter(
(todo: any) => todo.completed,
).length === e
);
}, expected);
}
async function checkTodosInLocalStorage(page: Page, title: string) {
return await page.waitForFunction((t) => {
return JSON.parse(localStorage["react-todos"])
.map((todo: any) => todo.title)
.includes(t);
}, title);
}

View File

@@ -1,20 +0,0 @@
import { test, expect } from "@playwright/test";
test("has title", async ({ page }) => {
await page.goto("https://playwright.dev/");
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test("get started link", async ({ page }) => {
await page.goto("https://playwright.dev/");
// Click the get started link.
await page.getByRole("link", { name: "Get started" }).click();
// Expects page to have a heading with the name of Installation.
await expect(
page.getByRole("heading", { name: "Installation" }),
).toBeVisible();
});

View File

@@ -1,62 +0,0 @@
import { test, expect } from "@playwright/test";
import fs from "fs";
import path from "path";
// We import the module after setting up a temporary log path by monkey patching electron-log.
// Since the project primarily uses Playwright for tests, we leverage its expect assertion library.
// NOTE: This is a lightweight test that simulates the pruning logic indirectly by invoking the exported ongoingMemoryDump
// function and creating artificial heap snapshot files exceeding the threshold.
// Because ongoingMemoryDump sets an interval, we invoke its internal logic by importing the file and manually calling dumpMemoryStats.
// For simplicity and to avoid altering production code for testability, we replicate the size enforcement logic here and assert behavior.
function createDummySnapshots(dir: string, count: number, sizeBytes: number) {
fs.mkdirSync(dir, { recursive: true });
for (let i = 0; i < count; i++) {
const file = path.join(dir, `dummy-${i}.heapsnapshot`);
const fd = fs.openSync(file, "w");
// Write sizeBytes of zeros
const buf = Buffer.alloc(1024 * 1024, 0); // 1MB chunk
let written = 0;
while (written < sizeBytes) {
fs.writeSync(fd, buf, 0, Math.min(buf.length, sizeBytes - written));
written += Math.min(buf.length, sizeBytes - written);
}
fs.closeSync(fd);
// Stagger mtime for deterministic pruning ordering
const mtime = new Date(Date.now() - (count - i) * 1000);
fs.utimesSync(file, mtime, mtime);
}
}
test("heap snapshot directory pruning reduces size below simulated hard cap", async () => {
const baseDir = fs.mkdtempSync(path.join(process.cwd(), "heap-test-"));
const heapDir = path.join(baseDir, "heap-snapshots");
// Simulate oversize: 15 files of 5MB each = 75MB
createDummySnapshots(heapDir, 15, 5 * 1024 * 1024);
// Use smaller cap to keep test resource usage low.
const MAX_DIR_BYTES = 50 * 1024 * 1024; // 50MB simulated cap
const TARGET_REDUCED_BYTES = Math.floor(MAX_DIR_BYTES * 0.9);
const files = fs
.readdirSync(heapDir)
.filter((f) => f.endsWith(".heapsnapshot"));
let totalSize = 0;
const fileStats: Array<{ file: string; size: number; mtimeMs: number }> = [];
for (const file of files) {
const stat = fs.statSync(path.join(heapDir, file));
totalSize += stat.size;
fileStats.push({ file, size: stat.size, mtimeMs: stat.mtimeMs });
}
expect(totalSize).toBeGreaterThan(MAX_DIR_BYTES);
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
let bytesAfter = totalSize;
for (const info of fileStats) {
if (bytesAfter <= TARGET_REDUCED_BYTES) break;
fs.unlinkSync(path.join(heapDir, info.file));
bytesAfter -= info.size;
}
expect(bytesAfter).toBeLessThanOrEqual(TARGET_REDUCED_BYTES);
// Cleanup
fs.rmSync(baseDir, { recursive: true, force: true });
});