Strip out partner related functionality.
This commit is contained in:
14
.env.rome
14
.env.rome
@@ -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"
|
||||
22
README.md
22
README.md
@@ -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.
|
||||
@@ -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>
|
||||
@@ -1,3 +1,3 @@
|
||||
provider: s3
|
||||
bucket: imex-partner
|
||||
bucket: esdp
|
||||
region: ca-central-1
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}`,
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
22
package.json
22
package.json
@@ -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 |
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { FC } from "react";
|
||||
|
||||
const Home: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Home</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
Reference in New Issue
Block a user