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
|
# ESDP
|
||||||
An electron app that is replacing the existing Bodyshop Partner that was a C#/WPF Application.
|
|
||||||
|
|
||||||
The purpose of this application is to:
|
# Outstanding Todos
|
||||||
* Parse EMS files, and upload them to the IO back end.
|
* Update certificates and signing.
|
||||||
* Receive requests for EMS file parsing
|
* Create S3 upload buckets
|
||||||
|
* Create S3 EMS upload bucket.
|
||||||
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.
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>com.convenientbrands.bodyshop-desktop.keepalive</string>
|
<string>com.imex.esdp.keepalive</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>Shop Partner Keep Alive</string>
|
<string>Shop Partner Keep Alive</string>
|
||||||
<string>imexmedia://keep-alive</string>
|
<string>imexmedia://keep-alive</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>StartInterval</key>
|
<key>StartInterval</key>
|
||||||
<integer>${KEEP_ALIVE_INTERVAL_SECONDS}</integer>
|
<integer>${KEEP_ALIVE_INTERVAL_SECONDS}</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
provider: s3
|
provider: s3
|
||||||
bucket: imex-partner
|
bucket: esdp
|
||||||
region: ca-central-1
|
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
|
appId: com.imex.esdp
|
||||||
copyright: Convenient Brands, LLC.
|
copyright: ImEX Systems Inc.
|
||||||
productName: ImEX Shop Partner
|
productName: EMS Uploader
|
||||||
generateUpdatesFilesForAllChannels: true
|
generateUpdatesFilesForAllChannels: true
|
||||||
|
|
||||||
directories:
|
directories:
|
||||||
@@ -16,7 +16,7 @@ files:
|
|||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
executableName: ShopPartner
|
executableName: EMSUploader
|
||||||
icon: resources/icon.png
|
icon: resources/icon.png
|
||||||
azureSignOptions:
|
azureSignOptions:
|
||||||
endpoint: https://eus.codesigning.azure.net
|
endpoint: https://eus.codesigning.azure.net
|
||||||
@@ -24,7 +24,7 @@ win:
|
|||||||
codeSigningAccountName: ImEX
|
codeSigningAccountName: ImEX
|
||||||
publisherName: ImEX Systems Inc.
|
publisherName: ImEX Systems Inc.
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
artifactName: esdp-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
createDesktopShortcut: always
|
||||||
@@ -39,9 +39,9 @@ mac:
|
|||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
- CFBundleURLTypes:
|
- CFBundleURLTypes:
|
||||||
- CFBundleTypeRole: Viewer # More specific role for protocol handling
|
- CFBundleTypeRole: Viewer # More specific role for protocol handling
|
||||||
CFBundleURLName: com.convenientbrands.bodyshop-desktop-imex
|
CFBundleURLName: com.imex.esdp
|
||||||
CFBundleURLSchemes:
|
CFBundleURLSchemes:
|
||||||
- imexmedia
|
- esdp
|
||||||
target:
|
target:
|
||||||
- target: default
|
- target: default
|
||||||
arch:
|
arch:
|
||||||
@@ -50,11 +50,11 @@ mac:
|
|||||||
arch:
|
arch:
|
||||||
- x64
|
- x64
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
artifactName: esdp-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: imex-partner-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
artifactName: esdp-${env.ARTIFACT_SUFFIX}${arch}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: s3
|
provider: s3
|
||||||
bucket: imex-partner
|
bucket: esdp
|
||||||
region: ca-central-1
|
region: ca-central-1
|
||||||
@@ -11,12 +11,12 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
sentryVitePlugin({
|
sentryVitePlugin({
|
||||||
org: "imex",
|
org: "imex",
|
||||||
project: "imex-partner",
|
project: "esdp",
|
||||||
sourcemaps: {
|
sourcemaps: {
|
||||||
filesToDeleteAfterUpload: ["**.js.map"],
|
filesToDeleteAfterUpload: ["**.js.map"],
|
||||||
},
|
},
|
||||||
release: {
|
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",
|
"name": "esdp",
|
||||||
"version": "1.0.8",
|
"version": "0.0.1",
|
||||||
"description": "Shop Management System Partner",
|
"description": "EMS Uploader",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Convenient Brands, LLC",
|
"author": "ImEX Systems Inc.",
|
||||||
"homepage": "https://convenient-brands.com",
|
"homepage": "https://imexsystems.ca",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "eslint --cache .",
|
"lint": "eslint --cache .",
|
||||||
@@ -13,17 +13,13 @@
|
|||||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"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": "node deploy/set-artifact-name.js electron-vite build && node deploy/set-artifact-name.js electron-builder ",
|
||||||
"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:publish": "node deploy/set-artifact-name.js electron-vite build && node deploy/set-artifact-name.js electron-builder --publish always",
|
||||||
"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:linux": "node deploy/set-artifact-name.js electron-vite build && node deploy/set-artifact-name.js electron-builder --linux",
|
||||||
"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",
|
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"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: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: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: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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.13.6",
|
"@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 { autoUpdater } from "electron-updater";
|
||||||
import path, { join } from "path";
|
import path, { join } from "path";
|
||||||
import imexAppIcon from "../../resources/icon.png?asset";
|
import imexAppIcon from "../../resources/icon.png?asset";
|
||||||
import romeAppIcon from "../../resources/ro-icon.png?asset";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
default as ErrorTypeCheck,
|
default as ErrorTypeCheck,
|
||||||
@@ -22,11 +21,8 @@ import {
|
|||||||
} from "../util/errorTypeCheck";
|
} from "../util/errorTypeCheck";
|
||||||
import ipcTypes from "../util/ipcTypes.json";
|
import ipcTypes from "../util/ipcTypes.json";
|
||||||
import ImportJob from "./decoder/decoder";
|
import ImportJob from "./decoder/decoder";
|
||||||
import LocalServer from "./http-server/http-server";
|
|
||||||
import store from "./store/store";
|
import { dumpMemoryStatsToFile } from "../util/memUsage";
|
||||||
import { checkForAppUpdates } from "./util/checkForAppUpdates";
|
|
||||||
import { getMainWindow } from "./util/toRenderer";
|
|
||||||
import { GetAllEnvFiles } from "./watcher/watcher";
|
|
||||||
import {
|
import {
|
||||||
isKeepAliveAgentInstalled,
|
isKeepAliveAgentInstalled,
|
||||||
setupKeepAliveAgent,
|
setupKeepAliveAgent,
|
||||||
@@ -35,11 +31,13 @@ import {
|
|||||||
isKeepAliveTaskInstalled,
|
isKeepAliveTaskInstalled,
|
||||||
setupKeepAliveTask,
|
setupKeepAliveTask,
|
||||||
} from "./setup-keep-alive-task";
|
} from "./setup-keep-alive-task";
|
||||||
|
import store from "./store/store";
|
||||||
|
import { checkForAppUpdates } from "./util/checkForAppUpdates";
|
||||||
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
|
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
|
||||||
import ongoingMemoryDump, { dumpMemoryStatsToFile } from "../util/memUsage";
|
import { getMainWindow } from "./util/toRenderer";
|
||||||
|
import { GetAllEnvFiles } from "./watcher/watcher";
|
||||||
|
|
||||||
const appIconToUse =
|
const appIconToUse = imexAppIcon;
|
||||||
import.meta.env.VITE_COMPANY === "IMEX" ? imexAppIcon : romeAppIcon;
|
|
||||||
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
|
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}";
|
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
|
||||||
log.transports.file.maxSize = 50 * 1024 * 1024; // 50 MB
|
log.transports.file.maxSize = 50 * 1024 * 1024; // 50 MB
|
||||||
const isMac: boolean = process.platform === "darwin";
|
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 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
|
let isKeepAliveLaunch = false; // Track if launched via keep-alive
|
||||||
// Initialize the server
|
// Initialize the server
|
||||||
const localServer = new LocalServer();
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
@@ -95,7 +92,7 @@ function createWindow(): void {
|
|||||||
icon: appIconToUse,
|
icon: appIconToUse,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
title: "Shop Partner",
|
title: "EMS Uploader",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, "../preload/index.js"),
|
preload: join(__dirname, "../preload/index.js"),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
@@ -425,13 +422,6 @@ function createWindow(): void {
|
|||||||
if (!isKeepAliveLaunch) {
|
if (!isKeepAliveLaunch) {
|
||||||
mainWindow.show(); // Show only if not a keep-alive launch
|
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) => {
|
mainWindow.on("close", (event: Electron.Event) => {
|
||||||
@@ -476,7 +466,7 @@ app.whenReady().then(async () => {
|
|||||||
log.debug("App is ready, initializing shortcuts and protocol handlers.");
|
log.debug("App is ready, initializing shortcuts and protocol handlers.");
|
||||||
|
|
||||||
if (platform.isWindows) {
|
if (platform.isWindows) {
|
||||||
app.setAppUserModelId("Shop Partner");
|
app.setAppUserModelId("esdp");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
@@ -508,17 +498,8 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
//Dynamically load ipcMain handlers once ready.
|
//Dynamically load ipcMain handlers once ready.
|
||||||
try {
|
try {
|
||||||
const { initializeCronTasks } = await import("./ipc/ipcMainConfig");
|
await import("./ipc/ipcMainConfig");
|
||||||
log.debug("Successfully loaded 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) {
|
} catch (error) {
|
||||||
log.error("Fatal: Failed to load ipcMainConfig", {
|
log.error("Fatal: Failed to load ipcMainConfig", {
|
||||||
...ErrorTypeCheck(error),
|
...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.
|
//The update itself will run when the bodyshop record is queried to know what release channel to use.
|
||||||
openMainWindow();
|
openMainWindow();
|
||||||
ongoingMemoryDump();
|
|
||||||
|
|
||||||
app.on("activate", function () {
|
app.on("activate", function () {
|
||||||
openMainWindow();
|
openMainWindow();
|
||||||
@@ -661,7 +641,6 @@ ipcMain.on(ipcTypes.toMain.updates.apply, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function preQuitMethods(): void {
|
function preQuitMethods(): void {
|
||||||
localServer.stop();
|
|
||||||
const currentSetting = store.get("app.openOnStartup") as boolean;
|
const currentSetting = store.get("app.openOnStartup") as boolean;
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
app.setLoginItemSettings({
|
app.setLoginItemSettings({
|
||||||
|
|||||||
@@ -27,27 +27,6 @@ import {
|
|||||||
ipcMainHandleAuthStateChanged,
|
ipcMainHandleAuthStateChanged,
|
||||||
ipMainHandleResetPassword,
|
ipMainHandleResetPassword,
|
||||||
} from "./ipcMainHandler.user";
|
} 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
|
// Log all IPC messages and their payloads
|
||||||
const logIpcMessages = (): void => {
|
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...
|
// Existing IPC handlers...
|
||||||
|
|
||||||
ipcMain.on(ipcTypes.toMain.test, () =>
|
ipcMain.on(ipcTypes.toMain.test, () =>
|
||||||
@@ -230,25 +140,6 @@ ipcMain.handle(
|
|||||||
SettingsPaintScaleOutputPathSet,
|
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, () => {
|
ipcMain.handle(ipcTypes.toMain.user.getActiveShop, () => {
|
||||||
return store.get("app.bodyshop.shopname");
|
return store.get("app.bodyshop.shopname");
|
||||||
});
|
});
|
||||||
@@ -273,6 +164,4 @@ ipcMain.on(ipcTypes.toMain.updates.download, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export { initializeCronTasks };
|
|
||||||
|
|
||||||
logIpcMessages();
|
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: {
|
settings: {
|
||||||
runOnStartup: true,
|
runOnStartup: true,
|
||||||
filepaths: [],
|
filepaths: [],
|
||||||
ppcFilePath: null,
|
|
||||||
emsOutFilePath: null,
|
|
||||||
qbFilePath: "",
|
|
||||||
runWatcherOnStartup: true,
|
runWatcherOnStartup: true,
|
||||||
enableMemDebug: false,
|
|
||||||
polling: {
|
polling: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
interval: 30000,
|
interval: 30000,
|
||||||
@@ -25,9 +21,6 @@ const store = new Store({
|
|||||||
user: null,
|
user: null,
|
||||||
isTest: false,
|
isTest: false,
|
||||||
bodyshop: {},
|
bodyshop: {},
|
||||||
masterdata: {
|
|
||||||
opcodes: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Shop Partner</title>
|
<title>EMS Uploader</title>
|
||||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
<!-- <meta
|
<!-- <meta
|
||||||
http-equiv="Content-Security-Policy"
|
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 "@ant-design/v5-patch-for-react-19";
|
||||||
import { Layout, Skeleton, ConfigProvider, Badge } from "antd";
|
import { Badge, ConfigProvider, Layout, Skeleton } from "antd";
|
||||||
import { User } from "firebase/auth";
|
import { FC } from "react";
|
||||||
import { useEffect, useState, FC } from "react";
|
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { HashRouter, Route, Routes } from "react-router";
|
import { HashRouter, Route, Routes } from "react-router";
|
||||||
import ipcTypes from "../../util/ipcTypes.json";
|
|
||||||
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
|
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
|
||||||
import Settings from "./components/Settings/Settings";
|
import Settings from "./components/Settings/Settings";
|
||||||
import SignInForm from "./components/SignInForm/SignInForm";
|
|
||||||
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
|
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
|
||||||
import reduxStore from "./redux/redux-store";
|
import reduxStore from "./redux/redux-store";
|
||||||
import { auth } from "./util/firebase";
|
|
||||||
import { NotificationProvider } from "./util/notificationContext";
|
import { NotificationProvider } from "./util/notificationContext";
|
||||||
|
|
||||||
const App: FC = () => {
|
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();
|
const isTest = window.api.isTest();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,23 +30,19 @@ const App: FC = () => {
|
|||||||
<HashRouter>
|
<HashRouter>
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<Skeleton loading={user === false} active>
|
<Skeleton loading={false} active>
|
||||||
<Layout style={{ minHeight: "100vh" }}>
|
<Layout style={{ minHeight: "100vh" }}>
|
||||||
{!user ? (
|
<Badge.Ribbon
|
||||||
<SignInForm />
|
text={isTest && "Connected to Test"}
|
||||||
) : (
|
color={isTest ? "red" : undefined}
|
||||||
<Badge.Ribbon
|
>
|
||||||
text={isTest && "Connected to Test"}
|
<Layout.Content style={{ padding: "0 24px" }}>
|
||||||
color={isTest ? "red" : undefined}
|
<UpdateAvailable />
|
||||||
>
|
<Routes>
|
||||||
<Layout.Content style={{ padding: "0 24px" }}>
|
<Route path="/" element={<Settings />} />
|
||||||
<UpdateAvailable />
|
</Routes>
|
||||||
<Routes>
|
</Layout.Content>
|
||||||
<Route path="/" element={<Settings />} />
|
</Badge.Ribbon>
|
||||||
</Routes>
|
|
||||||
</Layout.Content>
|
|
||||||
</Badge.Ribbon>
|
|
||||||
)}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</NotificationProvider>
|
</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 { FC } from "react";
|
||||||
import SettingsWatchedPaths from "./Settings.WatchedPaths";
|
import SettingsWatchedPaths from "./Settings.WatchedPaths";
|
||||||
import SettingsWatcher from "./Settings.Watcher";
|
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 = {
|
const colSpans = {
|
||||||
md: 12, // Two columns on medium screens and above
|
md: 12, // Two columns on medium screens and above
|
||||||
sm: 24, // One column on small screens
|
sm: 24, // One column on small screens
|
||||||
};
|
};
|
||||||
|
|
||||||
const Settings: FC = () => {
|
const Settings: FC = () => {
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col {...colSpans}>
|
||||||
<Welcome />
|
<SettingsWatchedPaths />
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...colSpans}>
|
<Col {...colSpans}>
|
||||||
<SettingsWatchedPaths />
|
<SettingsWatcher />
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...colSpans}>
|
</Row>
|
||||||
<SettingsWatcher />
|
);
|
||||||
</Col>
|
|
||||||
<Col {...colSpans}>
|
|
||||||
<SettingsPpcFilepath />
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpans}>
|
|
||||||
<SettingsEmsOutFilePath />
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpans}>
|
|
||||||
<SettingsPaintScaleInputPaths />
|
|
||||||
</Col>
|
|
||||||
<Col {...colSpans}>
|
|
||||||
<SettingsPaintScaleOutputPaths />
|
|
||||||
</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