Merged in feature/IO-3066-1-scaffolding (pull request #2)

Major v1 release.
This commit is contained in:
Patrick Fic
2025-05-16 17:17:38 +00:00
188 changed files with 21855 additions and 1661 deletions

65
.circleci/config.yml Normal file
View File

@@ -0,0 +1,65 @@
version: 2.1
orbs:
win: circleci/windows@5.1.0
jobs:
partner-win-build:
executor:
name: win/default
parameters:
tenantID:
type: string
default: $AZURE_TENANT_ID
clientID:
type: string
default: $AZURE_CLIENT_ID
clientSecret:
type: string
default: $AZURE_CLIENT_SECRET
steps:
- checkout
- run:
name: Set up Node.js
command: |
nvm install 22
nvm use 22
- run:
name: Install Build Dependencies
command: |
choco install python visualstudio2022-workload-vctools -y
python -m pip install --upgrade pip setuptools wheel
# look for existing cache and restore if found
- restore_cache:
key: bsp-win-deps-{{ checksum "package-lock.json" }}
# install dependencies
- run:
name: install dependencies
command: npm install
- run:
name: Install NPM Packages
command: npm install
- save_cache:
key: bsp-win-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Build Electron App for Windows
environment:
AZURE_TENANT_ID: << parameters.tenantID >>
AZURE_CLIENT_ID: << parameters.clientID >>
AZURE_CLIENT_SECRET: << parameters.clientSecret >>
command: |
$env:AZURE_TENANT_ID = << parameters.tenantID >>
$env:AZURE_CLIENT_ID = << parameters.clientID >>
$env:AZURE_CLIENT_SECRET = << parameters.clientSecret >>
npm run build:win
# - store_artifacts:
# path: .\dist
workflows:
deploy_and_build:
jobs:
- partner-win-build:
filters:
branches:
only: main

14
.env.imex Normal file
View File

@@ -0,0 +1,14 @@
VITE_COMPANY=IMEX
# Fire Base Config
VITE_FIREBASE_CONFIG={"apiKey":"AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU","authDomain":"imex-prod.firebaseapp.com","databaseURL":"https://imex-prod.firebaseio.com","projectId":"imex-prod","storageBucket":"imex-prod.appspot.com","messagingSenderId":"253497221485","appId":"1:253497221485:web:3c81c483b94db84b227a64","measurementId":"G-NTWBKG2L0M"}
VITE_FIREBASE_CONFIG_TEST={ "apiKey":"AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c", "authDomain":"imex-test.firebaseapp.com", "projectId":"imex-test", "storageBucket":"imex-test.appspot.com", "messagingSenderId":"991923618608", "appId":"1:991923618608:web:633437569cdad78299bef5", "measurementId":"G-TW0XLZEH18"}
# GraphQL Config
VITE_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
VITE_GRAPHQL_ENDPOINT_TEST=https://db.test.bodyshop.app/v1/graphql
# Front End URL
VITE_FE_URL=https://imex.online
VITE_FE_URL_TEST=https://test.imex.online
# API Url
VITE_API_URL="https://api.imex.online"
VITE_API_TEST_URL="https://test.api.imex.online"

9
.env.local Normal file
View File

@@ -0,0 +1,9 @@
VITE_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_FIREBASE_CONFIG_TEST={ "apiKey":"AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c", "authDomain":"imex-test.firebaseapp.com", "projectId":"imex-test", "storageBucket":"imex-test.appspot.com", "messagingSenderId":"991923618608", "appId":"1:991923618608:web:633437569cdad78299bef5", "measurementId":"G-TW0XLZEH18"}
VITE_GRAPHQL_ENDPOINT_TEST=https://db.test.bodyshop.app/v1/graphql
VITE_COMPANY=IMEX
VITE_FE_URL=https://imex.online
VITE_FE_URL_TEST=https://test.imex.online
VITE_API_URL="http://localhost:4000"
VITE_API_TEST_URL="https://api.test.imex.online"

14
.env.rome Normal file
View File

@@ -0,0 +1,14 @@
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"

1
.eslintcache Normal file

File diff suppressed because one or more lines are too long

15
.gitignore vendored
View File

@@ -2,4 +2,17 @@ node_modules
dist
out
.DS_Store
*.log*
*.log*
/logs
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
# Build Files
macbuild.sh
# Sentry Config File
.env.sentry-build-plugin

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/bodyshop-desktop.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

12
.idea/material_theme_project_new.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-4002d172:18ee315e3ba:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/bodyshop-desktop.iml" filepath="$PROJECT_DIR$/.idea/bodyshop-desktop.iml" />
</modules>
</component>
</project>

6
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

65
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,65 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
},
"experimentalNetworking": "off"
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
},
{
"name": "Debug Main Process w/ Hot Reloading",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap", "--watch"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
},
{
"name": "Debug All (Hot Reload)",
"configurations": [
"Debug Main Process w/ Hot Reloading",
"Debug Renderer Process"
],
"presentation": {
"order": 1
}
}
]
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"prettier.trailingComma": "all",
"editor.formatOnSave": true
}

BIN
PartnerCert.p12 Normal file

Binary file not shown.

View File

@@ -1,34 +1,18 @@
# bodyshop-desktop
# Shop Partner
An electron app that is replacing the existing Bodyshop Partner that was a C#/WPF Application.
An Electron application with React and TypeScript
The purpose of this application is to:
* Parse EMS files, and upload them to the IO back end.
* Receive requests for EMS file parsing
The following functionality will be coming:
* Interact with QuickBooks desktop (Windows Only)
* Paint scale integrations
* Parts Price Changes for CCC
## Recommended IDE Setup
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.
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
## Dev and Build Notes
Unlike the main app, the dev mode will only connect to ImEX Online test data.
## Project Setup
### Install
```bash
$ npm install
```
### Development
```bash
$ npm run dev
```
### Build
```bash
# For windows
$ npm run build:win
# For macOS
$ npm run build:mac
# For Linux
$ npm run build:linux
```
Building the app will require specifying the company to build for. Those details are captured in their respective ENV and YAML files.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 320 KiB

View File

@@ -1,3 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: bodyshop-desktop-updater
provider: s3
bucket: imex-partner
region: ca-central-1

67
electron-builder.imex.yml Normal file
View File

@@ -0,0 +1,67 @@
appId: com.convenientbrands.bodyshop-desktop-imex
copyright: Convenient Brands, LLC.
productName: ImEX 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/icon.png
azureSignOptions:
endpoint: https://eus.codesigning.azure.net
certificateProfileName: ImEXRPS
codeSigningAccountName: ImEX
nsis:
artifactName: ${name}-${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
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-imex
CFBundleURLSchemes:
- imexmedia
target:
- target: default
arch:
- arm64
- target: default
arch:
- x64
dmg:
artifactName: ${name}-${arch}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
desktop: scripts/imex-shop-partner.desktop
appImage:
artifactName: ${name}-${arch}.${ext}
npmRebuild: false
publish:
provider: s3
bucket: imex-partner
region: ca-central-1

67
electron-builder.rome.yml Normal file
View File

@@ -0,0 +1,67 @@
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/icon.png
azureSignOptions:
endpoint: https://eus.codesigning.azure.net
certificateProfileName: ImEXRPS
codeSigningAccountName: ImEX
nsis:
artifactName: ${name}-${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
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: ${name}-${arch}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
desktop: scripts/rome-shop-partner.desktop
appImage:
artifactName: ${name}-${arch}.${ext}
npmRebuild: false
publish:
provider: s3
bucket: rome-partner
region: us-east-2

View File

@@ -1,45 +0,0 @@
appId: com.convenientbrands.bodyshop-desktop
productName: Shop Partner
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!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: bodyshop-desktop
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
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.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

View File

@@ -1,20 +1,41 @@
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
import { sentryVitePlugin } from "@sentry/vite-plugin";
import { resolve } from "path";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
plugins: [
externalizeDepsPlugin(),
sentryVitePlugin({
org: "imex",
project: "imex-partner",
}),
],
build: {
sourcemap: true,
},
},
preload: {
plugins: [externalizeDepsPlugin()]
plugins: [externalizeDepsPlugin()],
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
}
"@renderer": resolve("src/renderer/src"),
},
},
plugins: [react()]
}
})
plugins: [
react(),
sentryVitePlugin({
org: "imex",
project: "imex-partner",
}),
],
build: {
sourcemap: true,
},
},
});

View File

@@ -1,31 +1,39 @@
import tseslint from '@electron-toolkit/eslint-config-ts'
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
import eslintPluginReact from 'eslint-plugin-react'
import eslintPluginReactHooks from 'eslint-plugin-react-hooks'
import eslintPluginReactRefresh from 'eslint-plugin-react-refresh'
import eslintConfigPrettier from "@electron-toolkit/eslint-config-prettier";
import tseslint from "@electron-toolkit/eslint-config-ts";
import eslintPluginReact from "eslint-plugin-react";
import eslintPluginReactHooks from "eslint-plugin-react-hooks";
import eslintPluginReactRefresh from "eslint-plugin-react-refresh";
export default tseslint.config(
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
tseslint.configs.recommended,
eslintPluginReact.configs.flat.recommended,
eslintPluginReact.configs.flat['jsx-runtime'],
{
settings: {
react: {
version: 'detect'
}
}
},
{
files: ['**/*.{ts,tsx}'],
plugins: {
'react-hooks': eslintPluginReactHooks,
'react-refresh': eslintPluginReactRefresh
{ignores: ["**/node_modules", "**/dist", "**/out"]},
tseslint.configs.recommended,
eslintPluginReact.configs.flat.recommended,
eslintPluginReact.configs.flat["jsx-runtime"],
{
settings: {
react: {
version: "detect",
},
},
},
rules: {
...eslintPluginReactHooks.configs.recommended.rules,
...eslintPluginReactRefresh.configs.vite.rules
}
},
eslintConfigPrettier
{
files: ["**/*.{ts,tsx}"],
plugins: {
"react-hooks": eslintPluginReactHooks,
"react-refresh": eslintPluginReactRefresh,
},
rules: {
...eslintPluginReactHooks.configs.recommended.rules,
...eslintPluginReactRefresh.configs.vite.rules,
},
},
{
files: ["**/*.{js,mjs,ts,tsx,jsx,tsx}"],
rules: {
"prettier/prettier": ["error", {"endOfLine": "off"}]
}
},
eslintConfigPrettier,
)

7770
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "bodyshop-desktop",
"version": "1.0.0",
"description": "An Electron application with React and TypeScript",
"description": "Shop Management System Partner",
"main": "./out/main/index.js",
"author": "example.com",
"homepage": "https://electron-vite.org",
"author": "Convenient Brands, LLC",
"homepage": "https://convenient-brands.com",
"scripts": {
"format": "prettier --write .",
"lint": "eslint --cache .",
@@ -13,37 +13,79 @@
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"build:imex": "electron-vite build --mode imex && electron-builder --config electron-builder.imex.yml",
"build:rome": "electron-vite build --mode rome && electron-builder --config electron-builder.rome.yml",
"build:imex:publish": "electron-vite build --mode imex && electron-builder --config electron-builder.imex.yml --publish always",
"build:rome:publish": "electron-vite build --mode rome && electron-builder --config electron-builder.rome.yml --publish always",
"build:imex:linux": "electron-vite build --mode imex && electron-builder --config electron-builder.imex.yml --linux",
"build:rome:linux": "electron-vite build --mode rome && electron-builder --config electron-builder.rome.yml --linux",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux"
"build:unpack": "electron-vite build --mode imex && electron-builder --dir",
"build:win": "electron-vite build --mode imex && electron-builder --win",
"build:mac": "electron-vite build --mode imex && electron-builder --mac",
"build:linux": "electron-vite build --mode imex && electron-builder --linux"
},
"dependencies": {
"@apollo/client": "^3.13.6",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^4.0.0",
"electron-updater": "^6.3.9"
"@sentry/electron": "^6.5.0",
"@sentry/vite-plugin": "^3.3.1",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"electron-log": "^5.3.3",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
"winax": "^3.6.2"
},
"devDependencies": {
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/node": "^22.13.10",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@playwright/test": "^1.51.1",
"@reduxjs/toolkit": "^2.6.1",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.1",
"@types/lodash": "^4.17.16",
"@types/node": "^22.14.0",
"@types/node-cron": "^3.0.11",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.2",
"@types/xml2js": "^0.4.14",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^35.0.1",
"antd": "^5.24.6",
"archiver": "^7.0.1",
"chokidar": "^4.0.3",
"cors": "^2.8.5",
"dbffile": "^1.12.0",
"electron": "^35.1.5",
"electron-builder": "^25.1.8",
"electron-vite": "^3.0.0",
"eslint": "^9.22.0",
"eslint-plugin-react": "^7.37.4",
"electron-store": "^8.2.0",
"electron-vite": "^3.1.0",
"eslint": "^9.24.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"express": "^5.1.0",
"firebase": "^11.6.0",
"graphql": "^16.10.0",
"graphql-request": "^7.1.2",
"i18next": "^24.2.3",
"lodash": "^4.17.21",
"node-cron": "^3.0.3",
"playwright": "^1.51.1",
"prettier": "^3.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.8.2",
"vite": "^6.2.1"
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-error-boundary": "^5.0.0",
"react-i18next": "^15.4.1",
"react-redux": "^9.2.0",
"react-router": "^7.5.0",
"redux-logger": "^3.0.6",
"typescript": "^5.8.3",
"vite": "6.2.6",
"xml2js": "^0.6.2",
"xmlbuilder2": "^3.1.1"
}
}

80
playwright.config.ts Normal file
View File

@@ -0,0 +1,80 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
//testDir: './tests/**/*',
//testMatch: '**/*.test.ts',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: "npm run start",
// url: "http://127.0.0.1:3000",
// reuseExistingServer: !process.env.CI,
// },
});

Binary file not shown.

Binary file not shown.

BIN
resources/QBFC16.dll Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 320 KiB

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Name=ImEX Shop Partner
Exec=/opt/ImEX%20Shop%20Partner/ShopPartner %u
Type=Application
Terminal=false
Icon=imex-shop-partner
Categories=Utility;
MimeType=x-scheme-handler/imexmedia;

13
scripts/installer.nsh Normal file
View File

@@ -0,0 +1,13 @@
!macro customInstall
; Register imexmedia protocol
WriteRegStr HKCR "imexmedia" "" "URL:ImEX Shop Partner Protocol"
WriteRegStr HKCR "imexmedia" "URL Protocol" ""
WriteRegStr HKCR "imexmedia\\shell" "" ""
WriteRegStr HKCR "imexmedia\\shell\\open" "" ""
WriteRegStr HKCR "imexmedia\\shell\\open\\command" "" '"$INSTDIR\ShopPartner.exe" "%1"'
!macroend
!macro customUnInstall
; Remove imexmedia protocol
DeleteRegKey HKCR "imexmedia"
!macroend

View File

@@ -0,0 +1,8 @@
[Desktop Entry]
Name=Rome Shop Partner
Exec=/opt/Rome%20Shop%20Partner/ShopPartner %u
Type=Application
Terminal=false
Icon=rome-shop-partner
Categories=Utility;
MimeType=x-scheme-handler/imexmedia;

12
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
export interface ImportMetaEnv {
readonly VITE_FIREBASE_CONFIG: string;
readonly VITE_GRAPHQL_ENDPOINT: string;
readonly VITE_FIREBASE_CONFIG_TEST: string;
readonly VITE_GRAPHQL_ENDPOINT_TEST: string;
}
export interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
export interface DecodedAD2 {
clmt_ln?: string;
clmt_fn?: string;
clmt_title?: string;
clmt_co_nm?: string;
clmt_addr1?: string;
clmt_addr2?: string;
clmt_city?: string;
clmt_st?: string;
clmt_zip?: string;
clmt_ctry?: string;
clmt_ph1?: string;
clmt_ph2?: string;
clmt_ea?: string;
est_co_id?: string;
est_co_nm?: string;
est_addr1?: string;
est_addr2?: string;
est_city?: string;
est_st?: string;
est_zip?: string;
est_ctry?: string;
est_ph1?: string;
est_ct_ln?: string;
est_ct_fn?: string;
est_ea?: string;
date_estimated?: Date; // This is transformed from insp_date
insp_date?: Date; // This exists initially but gets deleted
}

View File

@@ -0,0 +1,170 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import { DecodedAD2 } from "./decode-ad2.interface";
import { platform } from "@electron-toolkit/utils";
import errorTypeCheck from "../../util/errorTypeCheck";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeAD2 = async (
extensionlessFilePath: string,
): Promise<DecodedAD2> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}B.AD2`);
} catch {
dbf = await DBFFile.open(`${extensionlessFilePath}.AD2`);
}
if (!dbf) {
log.error(`Could not find any AD2 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD2 files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["b.ad2", ".ad2"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any AD2 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD2 files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening AD2 File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawAd2Data: DecodedAD2 = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"CLMT_LN", //TODO: This claimant info shouldnt be passed back. Just for the owner info.
"CLMT_FN",
"CLMT_TITLE",
"CLMT_CO_NM",
"CLMT_ADDR1",
"CLMT_ADDR2",
"CLMT_CITY",
"CLMT_ST",
"CLMT_ZIP",
"CLMT_CTRY",
"CLMT_PH1",
//"CLMT_PH1X",
"CLMT_PH2",
//"CLMT_PH2X",
//"CLMT_FAX",
//"CLMT_FAXX",
"CLMT_EA",
//"EST_CO_ID",
"EST_CO_NM",
"EST_ADDR1",
"EST_ADDR2",
"EST_CITY",
"EST_ST",
"EST_ZIP",
"EST_CTRY",
"EST_PH1",
//"EST_PH1X",
//"EST_PH2",
//"EST_PH2X",
//"EST_FAX",
//"EST_FAXX",
"EST_CT_LN",
"EST_CT_FN",
"EST_EA",
//"EST_LIC_NO",
//"EST_FILENO",
//"INSP_CT_LN",
//"INSP_CT_FN",
//"INSP_ADDR1",
//"INSP_ADDR2",
//"INSP_CITY",
//"INSP_ST",
//"INSP_ZIP",
//"INSP_CTRY",
//"INSP_PH1",
//"INSP_PH1X",
//"INSP_PH2",
//"INSP_PH2X",
//"INSP_FAX",
//"INSP_FAXX",
//"INSP_EA",
//"INSP_CODE",
//"INSP_DESC",
"INSP_DATE", //RENAME TO date_estimated
//"INSP_TIME",
//"RF_CO_ID",
//"RF_CO_NM",
//"RF_ADDR1",
//"RF_ADDR2",
//"RF_CITY",
//"RF_ST",
//"RF_ZIP",
//"RF_CTRY",
//"RF_PH1",
//"RF_PH1X",
//"RF_PH2",
//"RF_PH2X",
//"RF_FAX",
//"RF_FAXX",
//"RF_CT_LN",
//"RF_CT_FN",
//"RF_EA",
//"RF_TAX_ID",
//"RF_LIC_NO",
//"RF_BAR_NO",
//"RO_IN_DATE",
//"RO_IN_TIME",
//"TAR_DATE",
//"TAR_TIME",
//"RO_CMPDATE",
//"RO_CMPTIME",
//"DATE_OUT",
//"TIME_OUT",
//"RF_ESTIMTR",
//"MKTG_TYPE",
//"MKTG_SRC",
//"LOC_NM",
//"LOC_ADDR1",
//"LOC_ADDR2",
//"LOC_CITY",
//"LOC_ST",
//"LOC_ZIP",
//"LOC_CTRY",
//"LOC_PH1",
//"LOC_PH1X",
//"LOC_PH2",
//"LOC_PH2X",
//"LOC_FAX",
//"LOC_FAXX",
//"LOC_CT_LN",
//"LOC_CT_FN",
//"LOC_TITLE",
//"LOC_PH",
//"LOC_PHX",
//"LOC_EA",
]),
);
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
rawAd2Data.date_estimated = rawAd2Data.insp_date;
delete rawAd2Data.insp_date;
return rawAd2Data;
};
export default DecodeAD2;

View File

@@ -0,0 +1,5 @@
export interface DecodedEnv {
est_system?: string;
estfile_id?: string;
ciecaid?: string;
}

View File

@@ -0,0 +1,68 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedEnv } from "./decode-env.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeEnv = async (
extensionlessFilePath: string,
): Promise<DecodedEnv> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.ENV`);
} catch (error) {
log.error("Error opening ENV File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any ENV files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any ENV files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".env"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any ENV files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any ENV files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening ENV File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//TODO: Determine if there's any value to capture the whole ENV file.
const rawEnvData: DecodedEnv = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
//TODO: Several of these fields will fail. Should extend schema to capture them.
//"EST_SYSTEM",
"ESTFILE_ID",
]),
);
rawEnvData.ciecaid = rawEnvData.estfile_id;
delete rawEnvData.estfile_id;
//Apply business logic transfomrations.
return rawEnvData;
};
export default DecodeEnv;

View File

@@ -0,0 +1,54 @@
export interface DecodedLinLine {
line_no?: string;
line_ind?: string;
line_ref?: string;
tran_code?: string;
db_ref?: string;
unq_seq?: string;
//who_pays?: string;
line_desc?: string;
part_type?: string;
//part_desc_j?: boolean;
glass_flag?: boolean;
oem_partno?: string;
price_inc?: boolean;
alt_part_i?: boolean;
tax_part?: boolean;
db_price?: number;
act_price?: number;
price_j?: boolean;
cert_part?: boolean;
part_qty?: number;
alt_co_id?: string;
alt_partno?: string;
alt_overrd?: boolean;
alt_partm?: string;
prt_dsmk_p?: string;
prt_dsmk_m?: string;
mod_lbr_ty?: string;
db_hrs?: number;
mod_lb_hrs?: number;
lbr_inc?: boolean;
lbr_op?: string;
lbr_hrs_j?: boolean;
lbr_typ_j?: boolean;
lbr_op_j?: boolean;
paint_stg?: string;
paint_tone?: string;
lbr_tax?: boolean;
lbr_amt?: number;
misc_amt?: number;
misc_sublt?: string;
misc_tax?: boolean;
bett_type?: string;
bett_pctg?: string | number;
bett_amt?: number;
bett_tax?: boolean;
op_code_desc?: string;
}
export interface DecodedLin {
joblines: {
data: DecodedLinLine[];
};
}

View File

@@ -0,0 +1,120 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import { DecodedLin, DecodedLinLine } from "./decode-lin.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeLin = async (
extensionlessFilePath: string,
): Promise<DecodedLin> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.LIN`);
} catch (error) {
//LIN File only has 1 location.
log.error("Error opening LIN File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any LIN files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any LIN files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["lin"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any LIN files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any LIN files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening LIN File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const opCodeData = store.get("app.masterdata.opcodes"); //TODO: Type the op codes
const rawLinData: DecodedLinLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedLinLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"LINE_NO",
"LINE_IND",
"LINE_REF",
"TRAN_CODE",
"DB_REF",
"UNQ_SEQ",
// "WHO_PAYS",
"LINE_DESC",
"PART_TYPE",
//TODO: Believe this was previously broken in partner. Need to confirm.
// system == "M" ? "PART_DESCJ" : "PART_DES_J",
//"PART_DESC_J",
//End Check
"GLASS_FLAG",
"OEM_PARTNO",
"PRICE_INC",
"ALT_PART_I",
"TAX_PART",
"DB_PRICE",
"ACT_PRICE",
"PRICE_J",
"CERT_PART",
"PART_QTY",
"ALT_CO_ID",
"ALT_PARTNO",
"ALT_OVERRD",
"ALT_PARTM",
"PRT_DSMK_P",
"PRT_DSMK_M",
"MOD_LBR_TY",
"DB_HRS",
"MOD_LB_HRS",
"LBR_INC",
"LBR_OP",
"LBR_HRS_J",
"LBR_TYP_J",
"LBR_OP_J",
"PAINT_STG",
"PAINT_TONE",
"LBR_TAX",
"LBR_AMT",
"MISC_AMT",
"MISC_SUBLT",
"MISC_TAX",
"BETT_TYPE",
"BETT_PCTG",
"BETT_AMT",
"BETT_TAX",
]),
);
//Apply line by line adjustments.
singleLineData.op_code_desc = opCodeData[singleLineData.lbr_op]?.desc;
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { joblines: { data: rawLinData } };
};
export default DecodeLin;

View File

@@ -0,0 +1,15 @@
export interface DecodedPfh {
tax_prethr: number;
tax_thr_amt?: number;
tax_pstthr?: number;
tax_tow_rt: number;
tax_str_rt: number;
tax_sub_rt: number;
tax_lbr_rt: number;
federal_tax_rate: number;
adj_g_disc?: number;
adj_towdis?: number;
adj_strdis?: number;
tax_predis?: number;
tax_gst_rt?: number;
}

View File

@@ -0,0 +1,94 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfh } from "./decode-pfh.interface";
import { platform } from "os";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfh = async (
extensionlessFilePath: string,
): Promise<DecodedPfh> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
log.log("Trying to find PFH file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any PFH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFH files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfh"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFH files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfhData: DecodedPfh = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
//TODO: Several of these fields will fail. Should extend schema to capture them.
//"ID_PRO_NAM", //Remove
"TAX_PRETHR",
"TAX_THRAMT",
"TAX_PSTTHR",
//"TAX_TOW_IN", //Remove
"TAX_TOW_RT",
//"TAX_STR_IN", //Remove
"TAX_STR_RT",
//"TAX_SUB_IN", //Remove
"TAX_SUB_RT",
//"TAX_BTR_IN", //Remove
"TAX_LBR_RT",
"TAX_GST_RT",
//"TAX_GST_IN", //Remove
"ADJ_G_DISC",
"ADJ_TOWDIS",
"ADJ_STRDIS",
//"ADJ_BTR_IN", //Remove
"TAX_PREDIS",
]),
);
//Apply business logic transfomrations.
//Standardize some of the numbers and divide by 100.
rawPfhData.tax_prethr = (rawPfhData.tax_prethr ?? 0) / 100;
rawPfhData.tax_pstthr = (rawPfhData.tax_pstthr ?? 0) / 100;
rawPfhData.tax_tow_rt = (rawPfhData.tax_tow_rt ?? 0) / 100;
rawPfhData.tax_str_rt = (rawPfhData.tax_str_rt ?? 0) / 100;
rawPfhData.tax_sub_rt = (rawPfhData.tax_sub_rt ?? 0) / 100;
rawPfhData.tax_lbr_rt = (rawPfhData.tax_lbr_rt ?? 0) / 100;
rawPfhData.federal_tax_rate = (rawPfhData.tax_gst_rt ?? 0) / 100;
delete rawPfhData.tax_gst_rt;
return rawPfhData;
};
export default DecodePfh;

View File

@@ -0,0 +1,57 @@
//TODO: Clean up this interface. A bit messy as we store the data in very different ways.
export interface DecodedPflLine {
lbr_type: string;
lbr_desc: string;
lbr_rate: number;
lbr_tax_in: boolean;
lbr_taxp: number;
lbr_adjP: number;
lbr_tx_ty1: string;
lbr_tx_in1: boolean;
lbr_tx_ty2: string;
lbr_tx_in2: boolean;
lbr_tx_ty3: string;
lbr_tx_in3: boolean;
lbr_tx_ty4: string;
lbr_tx_in4: boolean;
lbr_tx_ty5: string;
lbr_tx_in5: boolean;
}
export interface JobLaborRateFields {
rate_laa: number;
rate_lab: number;
rate_lad: number;
rate_las: number;
rate_lar: number;
rate_lae: number;
rate_lag: number;
rate_laf: number;
rate_lam: number;
rate_lau: number;
rate_la1: number;
rate_la2: number;
rate_la3: number;
rate_la4: number;
}
export interface CiecaPfl {
LAA?: DecodedPflLine;
LAB?: DecodedPflLine;
LAD?: DecodedPflLine;
LAS?: DecodedPflLine;
LAR?: DecodedPflLine;
LAE?: DecodedPflLine;
LAG?: DecodedPflLine;
LAF?: DecodedPflLine;
LAM?: DecodedPflLine;
LAU?: DecodedPflLine;
LA1?: DecodedPflLine;
LA2?: DecodedPflLine;
LA3?: DecodedPflLine;
LA4?: DecodedPflLine;
}
export interface DecodedPfl extends JobLaborRateFields {
cieca_pfl: CiecaPfl;
}

View File

@@ -0,0 +1,122 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import {
DecodedPfl,
JobLaborRateFields,
DecodedPflLine,
} from "./decode-pfl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfl = async (
extensionlessFilePath: string,
): Promise<DecodedPfl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFL`);
} catch (error) {
//PFL File only has 1 location.
log.error("Error opening PFL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const jobLaborRates: JobLaborRateFields = {
rate_laa: 0,
rate_lab: 0,
rate_lad: 0,
rate_las: 0,
rate_lar: 0,
rate_lae: 0,
rate_lag: 0,
rate_laf: 0,
rate_lam: 0,
rate_lau: 0,
rate_la1: 0,
rate_la2: 0,
rate_la3: 0,
rate_la4: 0,
};
const rawPflData: DecodedPflLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPflLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"LBR_TYPE",
"LBR_DESC",
"LBR_RATE",
"LBR_TAX_IN",
"LBR_TAXP",
"LBR_ADJP",
"LBR_TX_TY1",
"LBR_TX_IN1",
"LBR_TX_TY2",
"LBR_TX_IN2",
"LBR_TX_TY3",
"LBR_TX_IN3",
"LBR_TX_TY4",
"LBR_TX_IN4",
"LBR_TX_TY5",
"LBR_TX_IN5",
]),
);
//Apply line by line adjustments.
//Set the job.rate_<CIECA_TYPE> field based on the value.
jobLaborRates[`rate_${singleLineData.lbr_type.toLowerCase()}`] =
singleLineData.lbr_rate;
//For Mitchell, Alum is stored under LA3 instead of LAA. Shift it back over.
//The old partner had a check for this, but it always was true. Matching that logic.
if (singleLineData.lbr_type === "LA3") {
jobLaborRates[`rate_laa`] = singleLineData.lbr_rate;
}
//Also capture the whole object.
//This is segmented because the whole object was not previously captured for ImEX as it wasn't needed.
//Rome needs the whole object to accurately calculate the tax rates.
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
const pflObj = _.keyBy(rawPflData, "lbr_type");
return { ...jobLaborRates, cieca_pfl: pflObj };
};
export default DecodePfl;

View File

@@ -0,0 +1,50 @@
export interface DecodedPfmLine {
matl_type?: string;
cal_code?: number;
cal_desc?: string;
cal_maxdlr?: number;
cal_prip?: number;
cal_secp?: number;
mat_calp?: number;
cal_prethr?: number;
cal_pstthr?: number;
cal_thramt?: number;
cal_lbrmin?: number;
cal_lbrrte?: number;
cal_opcode?: string;
tax_ind?: boolean;
mat_taxp?: number;
mat_adjp?: number;
mat_tx_ty1?: string;
mat_tx_in1?: boolean;
mat_tx_ty2?: string;
mat_tx_in2?: boolean;
mat_tx_ty3?: string;
mat_tx_in3?: boolean;
mat_tx_ty4?: string;
mat_tx_in4?: boolean;
mat_tx_ty5?: string;
mat_tx_in5?: boolean;
}
export interface JobMaterialRateFields {
rate_mapa: number;
tax_paint_mat_rt: number;
rate_mash: number;
tax_shop_mat_rt: number;
rate_mahw: number;
tax_levies_rt: number;
rate_ma2s: number;
rate_ma2t: number;
rate_ma3s: number;
rate_macs: number;
rate_mabl: number;
}
export interface DecodedPfm extends JobMaterialRateFields {
materials: {
MAPA?: DecodedPfmLine;
MASH?: DecodedPfmLine;
};
cieca_pfm?: DecodedPfmLine[];
}

View File

@@ -0,0 +1,169 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import {
DecodedPfm,
DecodedPfmLine,
JobMaterialRateFields,
} from "./decode-pfm.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfm = async (
extensionlessFilePath: string,
): Promise<DecodedPfm> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFM`);
} catch (error) {
//PFM File only has 1 location.
log.error("Error opening PFM File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFM files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFM files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfm"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFM files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFM files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFM File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const jobMaterialRates: JobMaterialRateFields = {
rate_mapa: 0,
tax_paint_mat_rt: 0,
rate_mash: 0,
tax_shop_mat_rt: 0,
rate_mahw: 0,
tax_levies_rt: 0,
rate_ma2s: 0,
rate_ma2t: 0,
rate_ma3s: 0,
rate_macs: 0,
rate_mabl: 0,
};
const rawPfmData: DecodedPfmLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfmLine = YNBoolConverter(
deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"MATL_TYPE",
"CAL_CODE",
"CAL_DESC",
"CAL_MAXDLR",
"CAL_PRIP",
"CAL_SECP",
"MAT_CALP",
"CAL_PRETHR", //Mitchell here
"CAL_PSTTHR",
"CAL_THRAMT",
"CAL_LBRMIN",
"CAL_LBRRTE", //Audatex puts it here
"CAL_OPCODE",
"TAX_IND",
"MAT_TAXP",
"MAT_ADJP",
"MAT_TX_TY1",
"MAT_TX_IN1",
"MAT_TX_TY2",
"MAT_TX_IN2",
"MAT_TX_TY3",
"MAT_TX_IN3",
"MAT_TX_TY4",
"MAT_TX_IN4",
"MAT_TX_TY5",
"MAT_TX_IN5",
]),
),
);
//Also capture the whole object.
//This is segmented because the whole object was not previously captured for ImEX as it wasn't needed.
//Rome needs the whole object to accurately calculate the tax rates.
return singleLineData;
});
//Apply line by line adjustments.
const mapaLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAPA",
);
if (mapaLine) {
jobMaterialRates.rate_mapa =
mapaLine.cal_lbrrte || mapaLine.cal_prethr || 0;
jobMaterialRates.tax_paint_mat_rt = (mapaLine.mat_taxp ?? 0) / 100;
}
const mashLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MASH",
);
if (mashLine) {
jobMaterialRates.rate_mash =
mashLine.cal_lbrrte || mashLine.cal_prethr || 0;
jobMaterialRates.tax_shop_mat_rt = (mashLine.mat_taxp ?? 0) / 100;
}
const mahwLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAHW",
);
if (mahwLine) {
jobMaterialRates.rate_mahw =
mahwLine.cal_lbrrte || mahwLine.cal_prethr || 0;
jobMaterialRates.tax_levies_rt = (mahwLine.mat_taxp ?? 0) / 100;
}
const additionalMaterials = ["MA2S", "MA2T", "MA3S", "MACS", "MABL"];
additionalMaterials.forEach((type) => {
const line: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === type,
);
if (line) {
jobMaterialRates[`rate_${type.toLowerCase()}`] =
line.cal_lbrrte || line.cal_prethr || 0;
}
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return {
...jobMaterialRates,
materials: {
MASH: mashLine,
MAPA: mapaLine, //TODO: Need to verify if more fields are to come in here.
},
//cieca_pfm: rawPfmData, //TODO: Not currently captured. This may have valu in the future.
};
};
export default DecodePfm;

View File

@@ -0,0 +1,32 @@
export interface DecodedPfoLine {
tx_tow_ty?: string;
tow_t_ty1?: string;
tow_t_in1?: boolean;
tow_t_ty2?: string;
tow_t_in2?: boolean;
tow_t_ty3?: string;
tow_t_in3?: boolean;
tow_t_ty4?: string;
tow_t_in4?: boolean;
tow_t_ty5?: string;
tow_t_in5?: boolean;
tow_t_ty6?: string;
tow_t_in6?: boolean;
tx_stor_ty?: string;
stor_t_ty1?: string;
stor_t_in1?: boolean;
stor_t_ty2?: string;
stor_t_in2?: boolean;
stor_t_ty3?: string;
stor_t_in3?: boolean;
stor_t_ty4?: string;
stor_t_in4?: boolean;
stor_t_ty5?: string;
stor_t_in5?: boolean;
stor_t_ty6?: string;
stor_t_in6?: boolean;
}
export interface DecodedPfo {
cieca_pfo: DecodedPfoLine;
}

View File

@@ -0,0 +1,93 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import { DecodedPfo, DecodedPfoLine } from "./decode-pfo.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfo = async (
extensionlessFilePath: string,
): Promise<DecodedPfo> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFO`);
} catch (error) {
log.error("Error opening PFO File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.PFO`);
log.log("Trying to find PFO file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any PFO files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFO files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfo"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFO files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFO files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFO File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFO will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfoData: DecodedPfoLine = YNBoolConverter(
deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TX_TOW_TY",
"TOW_T_TY1",
"TOW_T_IN1",
"TOW_T_TY2",
"TOW_T_IN2",
"TOW_T_TY3",
"TOW_T_IN3",
"TOW_T_TY4",
"TOW_T_IN4",
"TOW_T_TY5",
"TOW_T_IN5",
"TOW_T_TY6",
"TOW_T_IN6",
"TX_STOR_TY",
"STOR_T_TY1",
"STOR_T_IN1",
"STOR_T_TY2",
"STOR_T_IN2",
"STOR_T_TY3",
"STOR_T_IN3",
"STOR_T_TY4",
"STOR_T_IN4",
"STOR_T_TY5",
"STOR_T_IN5",
"STOR_T_TY6",
"STOR_T_IN6",
]),
),
);
//Apply business logic transfomrations.
return { cieca_pfo: rawPfoData };
};
export default DecodePfo;

View File

@@ -0,0 +1,37 @@
export interface DecodedPfpLine {
prt_type: string;
prt_tax_in: boolean;
prt_tax_rt: number;
prt_mkupp: number;
prt_mktyp: string;
prt_discp: number;
prt_tx_ty1: string;
prt_tx_in1: boolean;
prt_tx_ty2: string;
prt_tx_in2: boolean;
prt_tx_ty3: string;
prt_tx_in3: boolean;
prt_tx_ty4: string;
prt_tx_in4: boolean;
prt_tx_ty5: string;
prt_tx_in5: boolean;
}
export interface DecodedPfpLinesByType {
PAA: DecodedPfpLine;
PAC: DecodedPfpLine;
PAL: DecodedPfpLine;
PAG: DecodedPfpLine;
PAM: DecodedPfpLine;
PAP: DecodedPfpLine;
PAN: DecodedPfpLine;
PAO: DecodedPfpLine;
PAR: DecodedPfpLine;
PAS: DecodedPfpLine;
PASL: DecodedPfpLine;
PAT: DecodedPfpLine;
}
export interface DecodedPfp {
parts_tax_rates: DecodedPfpLinesByType;
}

View File

@@ -0,0 +1,98 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import {
DecodedPfp,
DecodedPfpLine,
DecodedPfpLinesByType,
} from "./decode-pfp.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfp = async (
extensionlessFilePath: string,
): Promise<DecodedPfp> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFP`);
} catch (error) {
//PFP File only has 1 location.
log.error("Error opening PFP File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFP files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFP files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfp"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFP files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFP files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFP File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfpData: DecodedPfpLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfpLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"PRT_TYPE",
"PRT_TAX_IN",
"PRT_TAX_RT",
"PRT_MKUPP",
"PRT_MKTYP",
"PRT_DISCP",
"PRT_TX_TY1",
"PRT_TX_IN1",
"PRT_TX_TY2",
"PRT_TX_IN2",
"PRT_TX_TY3",
"PRT_TX_IN3",
"PRT_TX_TY4",
"PRT_TX_IN4",
"PRT_TX_TY5",
"PRT_TX_IN5",
]),
);
singleLineData.prt_tax_rt = singleLineData.prt_tax_rt / 100;
return YNBoolConverter(singleLineData);
});
//Apply business logic transfomrations.
//Convert array of lines to a hash object.
const parsedPfpFile: DecodedPfpLinesByType = rawPfpData.reduce(
(acc: DecodedPfpLinesByType, line: DecodedPfpLine) => {
acc[line.prt_type] = line;
return acc;
},
{} as DecodedPfpLinesByType,
);
return { parts_tax_rates: parsedPfpFile };
};
export default DecodePfp;

View File

@@ -0,0 +1,147 @@
/**
* Interface representing decoded data from a PFT file
* Contains tax type information with up to 6 tax types and 5 tiers each
*/
export interface DecodedPftLine {
// Tax Type 1
tax_type1?: string;
ty1_tier1?: number;
ty1_thres1?: number;
ty1_rate1?: number;
ty1_sur1?: number;
ty1_tier2?: number;
ty1_thres2?: number;
ty1_rate2?: number;
ty1_sur2?: number;
ty1_tier3?: number;
ty1_thres3?: number;
ty1_rate3?: number;
ty1_sur3?: number;
ty1_tier4?: number;
ty1_thres4?: number;
ty1_rate4?: number;
ty1_sur4?: number;
ty1_tier5?: number;
ty1_thres5?: number;
ty1_rate5?: number;
ty1_sur5?: number;
// Tax Type 2
tax_type2?: string;
ty2_tier1?: number;
ty2_thres1?: number;
ty2_rate1?: number;
ty2_sur1?: number;
ty2_tier2?: number;
ty2_thres2?: number;
ty2_rate2?: number;
ty2_sur2?: number;
ty2_tier3?: number;
ty2_thres3?: number;
ty2_rate3?: number;
ty2_sur3?: number;
ty2_tier4?: number;
ty2_thres4?: number;
ty2_rate4?: number;
ty2_sur4?: number;
ty2_tier5?: number;
ty2_thres5?: number;
ty2_rate5?: number;
ty2_sur5?: number;
// Tax Type 3
tax_type3?: string;
ty3_tier1?: number;
ty3_thres1?: number;
ty3_rate1?: number;
ty3_sur1?: number;
ty3_tier2?: number;
ty3_thres2?: number;
ty3_rate2?: number;
ty3_sur2?: number;
ty3_tier3?: number;
ty3_thres3?: number;
ty3_rate3?: number;
ty3_sur3?: number;
ty3_tier4?: number;
ty3_thres4?: number;
ty3_rate4?: number;
ty3_sur4?: number;
ty3_tier5?: number;
ty3_thres5?: number;
ty3_rate5?: number;
ty3_sur5?: number;
// Tax Type 4
tax_type4?: string;
ty4_tier1?: number;
ty4_thres1?: number;
ty4_rate1?: number;
ty4_sur1?: number;
ty4_tier2?: number;
ty4_thres2?: number;
ty4_rate2?: number;
ty4_sur2?: number;
ty4_tier3?: number;
ty4_thres3?: number;
ty4_rate3?: number;
ty4_sur3?: number;
ty4_tier4?: number;
ty4_thres4?: number;
ty4_rate4?: number;
ty4_sur4?: number;
ty4_tier5?: number;
ty4_thres5?: number;
ty4_rate5?: number;
ty4_sur5?: number;
// Tax Type 5
tax_type5?: string;
ty5_tier1?: number;
ty5_thres1?: number;
ty5_rate1?: number;
ty5_sur1?: number;
ty5_tier2?: number;
ty5_thres2?: number;
ty5_rate2?: number;
ty5_sur2?: number;
ty5_tier3?: number;
ty5_thres3?: number;
ty5_rate3?: number;
ty5_sur3?: number;
ty5_tier4?: number;
ty5_thres4?: number;
ty5_rate4?: number;
ty5_sur4?: number;
ty5_tier5?: number;
ty5_thres5?: number;
ty5_rate5?: number;
ty5_sur5?: number;
// Tax Type 6
tax_type6?: string;
ty6_tier1?: number;
ty6_thres1?: number;
ty6_rate1?: number;
ty6_sur1?: number;
ty6_tier2?: number;
ty6_thres2?: number;
ty6_rate2?: number;
ty6_sur2?: number;
ty6_tier3?: number;
ty6_thres3?: number;
ty6_rate3?: number;
ty6_sur3?: number;
ty6_tier4?: number;
ty6_thres4?: number;
ty6_rate4?: number;
ty6_sur4?: number;
ty6_tier5?: number;
ty6_thres5?: number;
ty6_rate5?: number;
ty6_sur5?: number;
}
export interface DecodedPft {
cieca_pft: DecodedPftLine;
}

View File

@@ -0,0 +1,189 @@
import { platform } from "@electron-toolkit/utils";
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPft, DecodedPftLine } from "./decode-pft.interface";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePft = async (
extensionlessFilePath: string,
): Promise<DecodedPft> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFT`);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFT files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFT files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["pft"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFT files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFT files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFT File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFT will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPftData: DecodedPftLine = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TAX_TYPE1", //The below is is taken from a CCC estimate. Will require validation to ensure it is also accurate for Audatex/Mitchell
"TY1_TIER1",
"TY1_THRES1",
"TY1_RATE1",
"TY1_SUR1",
"TY1_TIER2",
"TY1_THRES2",
"TY1_RATE2",
"TY1_SUR2",
"TY1_TIER3",
"TY1_THRES3",
"TY1_RATE3",
"TY1_SUR3",
"TY1_TIER4",
"TY1_THRES4",
"TY1_RATE4",
"TY1_SUR4",
"TY1_TIER5",
"TY1_THRES5",
"TY1_RATE5",
"TY1_SUR5",
"TAX_TYPE2",
"TY2_TIER1",
"TY2_THRES1",
"TY2_RATE1",
"TY2_SUR1",
"TY2_TIER2",
"TY2_THRES2",
"TY2_RATE2",
"TY2_SUR2",
"TY2_TIER3",
"TY2_THRES3",
"TY2_RATE3",
"TY2_SUR3",
"TY2_TIER4",
"TY2_THRES4",
"TY2_RATE4",
"TY2_SUR4",
"TY2_TIER5",
"TY2_THRES5",
"TY2_RATE5",
"TY2_SUR5",
"TAX_TYPE3",
"TY3_TIER1",
"TY3_THRES1",
"TY3_RATE1",
"TY3_SUR1",
"TY3_TIER2",
"TY3_THRES2",
"TY3_RATE2",
"TY3_SUR2",
"TY3_TIER3",
"TY3_THRES3",
"TY3_RATE3",
"TY3_SUR3",
"TY3_TIER4",
"TY3_THRES4",
"TY3_RATE4",
"TY3_SUR4",
"TY3_TIER5",
"TY3_THRES5",
"TY3_RATE5",
"TY3_SUR5",
"TAX_TYPE4",
"TY4_TIER1",
"TY4_THRES1",
"TY4_RATE1",
"TY4_SUR1",
"TY4_TIER2",
"TY4_THRES2",
"TY4_RATE2",
"TY4_SUR2",
"TY4_TIER3",
"TY4_THRES3",
"TY4_RATE3",
"TY4_SUR3",
"TY4_TIER4",
"TY4_THRES4",
"TY4_RATE4",
"TY4_SUR4",
"TY4_TIER5",
"TY4_THRES5",
"TY4_RATE5",
"TY4_SUR5",
"TAX_TYPE5",
"TY5_TIER1",
"TY5_THRES1",
"TY5_RATE1",
"TY5_SUR1",
"TY5_TIER2",
"TY5_THRES2",
"TY5_RATE2",
"TY5_SUR2",
"TY5_TIER3",
"TY5_THRES3",
"TY5_RATE3",
"TY5_SUR3",
"TY5_TIER4",
"TY5_THRES4",
"TY5_RATE4",
"TY5_SUR4",
"TY5_TIER5",
"TY5_THRES5",
"TY5_RATE5",
"TY5_SUR5",
"TAX_TYPE6",
"TY6_TIER1",
"TY6_THRES1",
"TY6_RATE1",
"TY6_SUR1",
"TY6_TIER2",
"TY6_THRES2",
"TY6_RATE2",
"TY6_SUR2",
"TY6_TIER3",
"TY6_THRES3",
"TY6_RATE3",
"TY6_SUR3",
"TY6_TIER4",
"TY6_THRES4",
"TY6_RATE4",
"TY6_SUR4",
"TY6_TIER5",
"TY6_THRES5",
"TY6_RATE5",
"TY6_SUR5",
]),
);
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { cieca_pft: rawPftData };
};
export default DecodePft;

View File

@@ -0,0 +1,23 @@
export interface DecodedStlLine {
ttl_type?: string;
ttl_typecd?: string;
t_amt?: number;
t_hrs?: number;
t_addlbr?: number;
t_discamt?: number;
t_mkupamt?: number;
t_gdiscamt?: number;
tax_amt?: number;
nt_amt?: number;
nt_hrs?: number;
nt_addlbr?: number;
nt_disc?: number;
nt_mkup?: number;
nt_gdis?: number;
ttl_typamt?: number;
ttl_hrs?: number;
ttl_amt?: number;
}
export interface DecodedStl {
cieca_stl: { data: DecodedStlLine[] };
}

View File

@@ -0,0 +1,85 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedStl, DecodedStlLine } from "./decode-stl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeStl = async (
extensionlessFilePath: string,
): Promise<DecodedStl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.STL`);
} catch (error) {
log.error("Error opening STL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any STL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any STL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["stl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any STL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any STL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening STL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawStlData: DecodedStlLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedStlLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"TTL_TYPE",
"TTL_TYPECD",
"T_AMT",
"T_HRS",
"T_ADDLBR",
"T_DISCAMT",
"T_MKUPAMT",
"T_GDISCAMT",
"TAX_AMT",
"NT_AMT",
"NT_HRS",
"NT_ADDLBR",
"NT_DISC",
"NT_MKUP",
"NT_GDIS",
"TTL_TYPAMT",
"TTL_HRS",
"TTL_AMT",
]),
);
//Apply line by line adjustments.
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { cieca_stl: { data: rawStlData } };
};
export default DecodeStl;

View File

@@ -0,0 +1,22 @@
export interface DecodedTtl {
clm_total: number;
depreciation_taxes: number;
cieca_ttl: { data: DecodedTtlLine };
}
export interface DecodedTtlLine {
g_ttl_amt?: number;
g_bett_amt?: number;
g_rpd_amt?: number;
g_ded_amt?: number;
g_cust_amt?: number;
g_aa_amt?: number;
n_ttl_amt?: number;
prev_net?: number;
supp_amt?: number;
n_supp_amt?: number;
g_upd_amt?: number;
g_ttl_disc?: number;
g_tax?: number;
gst_amt?: number;
}

View File

@@ -0,0 +1,80 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedTtl, DecodedTtlLine } from "./decode-ttl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeTtl = async (
extensionlessFilePath: string,
): Promise<DecodedTtl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.TTL`);
} catch (error) {
log.error("Error opening TTL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any TTL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any TTL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["ttl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any TTL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any TTL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening TTL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFT will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawTtlData: DecodedTtlLine = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"G_TTL_AMT",
"G_BETT_AMT",
"G_RPD_AMT",
"G_DED_AMT",
"G_CUST_AMT",
"G_AA_AMT",
"N_TTL_AMT",
"PREV_NET",
"SUPP_AMT",
"N_SUPP_AMT", //Previously commented. Possible issue.
"G_UPD_AMT",
"G_TTL_DISC",
"G_TAX",
"GST_AMT",
]),
);
//Apply business logic transfomrations.
return {
clm_total: rawTtlData.g_ttl_amt || 0,
depreciation_taxes: rawTtlData.g_bett_amt || 0, //TODO: Find where this needs to be filled from
cieca_ttl: { data: rawTtlData },
};
};
export default DecodeTtl;

View File

@@ -0,0 +1,64 @@
import { UUID } from "crypto";
export interface DecodedVeh {
// Basic vehicle information
plate_no?: string;
plate_st?: string;
v_vin?: string;
v_model_yr?: string;
v_make_desc?: string;
v_model_desc?: string;
v_color?: string;
kmin?: number;
area_of_damage?: {
impact1?: string;
impact2?: string;
};
// Complete vehicle data object
vehicle?: { data: VehicleRecordInterface };
}
export interface VehicleRecordInterface {
// Area of damage information
area_of_damage?: {
impact1?: string;
impact2?: string;
};
// Paint code information
v_paint_codes: {
paint_cd1: string;
paint_cd2: string;
paint_cd3: string;
};
// Vehicle information from DBF file
db_v_code?: string;
plate_no?: string;
plate_st?: string;
v_vin?: string;
v_cond: string;
v_prod_dt?: Date;
v_model_yr: string;
v_makecode: string;
v_make_desc?: string;
v_model?: string;
v_model_desc?: string;
v_type: string;
v_bstyle?: string;
v_trimcode?: string;
trim_color?: string;
v_mldgcode?: string;
v_engine?: string;
v_mileage?: number; //TODO: This can sometimes come in as UNK.
v_color?: string;
v_tone?: string;
v_stage?: string;
shopid: UUID;
//These are removed during business logic processing.
v_makedesc?: string;
impact_1?: string;
impact_2?: string;
paint_cd1?: string;
paint_cd2?: string;
paint_cd3?: string;
}

View File

@@ -0,0 +1,145 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import { DecodedVeh, VehicleRecordInterface } from "./decode-veh.interface";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import typeCaster from "../../util/typeCaster";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeVeh = async (
extensionlessFilePath: string,
): Promise<DecodedVeh> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}V.VEH`);
} catch (error) {
log.error("Error opening VEH File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.VEH`);
log.log("Found VEH file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any VEH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any VEH files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["v.veh", ".veh"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any VEH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any VEH files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath, { readMode: "loose" });
} catch (error) {
log.error("Error opening VEH File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
//typeCaster is required as the previous partner sent some of these values toString, and the database was made accordingly rather than keeping their original type.
//Alternative is to change the database schema to match the original type.
const rawVehData: VehicleRecordInterface = typeCaster(
deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"IMPACT_1",
"IMPACT_2",
"DB_V_CODE",
"PLATE_NO",
"PLATE_ST",
"V_VIN",
"V_COND",
"V_PROD_DT",
"V_MODEL_YR",
"V_MAKECODE",
"V_MAKEDESC",
"V_MODEL",
"V_TYPE",
"V_BSTYLE",
"V_TRIMCODE",
"TRIM_COLOR",
"V_MLDGCODE",
"V_ENGINE",
"V_MILEAGE",
"V_COLOR",
"V_TONE",
"V_STAGE",
"PAINT_CD1",
"PAINT_CD2",
"PAINT_CD3",
]),
),
{
v_tone: "string",
v_stage: "string",
},
);
//Apply business logic transfomrations.
//An old error where the column had an extra underscore.
rawVehData.v_make_desc = rawVehData.v_makedesc || rawVehData.v_makecode; //Fallback for US.
delete rawVehData.v_makedesc;
//An old error where the column had an extra underscore.
rawVehData.v_model_desc = rawVehData.v_model;
delete rawVehData.v_model;
//Consolidate Area of Damage.
const area_of_damage = {
impact1: rawVehData.impact_1 ?? "",
impact2: rawVehData.impact_2 ?? "",
};
delete rawVehData.impact_1;
delete rawVehData.impact_2;
const kmin = rawVehData.v_mileage ?? 0;
delete rawVehData.v_mileage;
//Consolidate Paint Code information.
rawVehData.v_paint_codes = {
paint_cd1: rawVehData.paint_cd1 ?? "",
paint_cd2: rawVehData.paint_cd2 ?? "",
paint_cd3: rawVehData.paint_cd3 ?? "",
};
delete rawVehData.paint_cd1;
delete rawVehData.paint_cd2;
delete rawVehData.paint_cd3;
rawVehData.shopid = store.get("app.bodyshop.id");
//Aggregate the vehicle data to be stamped onto the job record.
const jobVehicleData: DecodedVeh = {
plate_no: rawVehData.plate_no,
plate_st: rawVehData.plate_st,
v_vin: rawVehData.v_vin,
v_model_yr: rawVehData.v_model_yr,
v_make_desc: rawVehData.v_make_desc,
v_model_desc: rawVehData.v_model_desc,
v_color: rawVehData.v_color,
kmin: kmin,
area_of_damage: area_of_damage,
vehicle: {
data: rawVehData,
},
};
return jobVehicleData;
};
export default DecodeVeh;

View File

@@ -0,0 +1,47 @@
import log from "electron-log/main";
import fs from "fs";
import path from "path";
const findFileCaseInsensitive = async (
extensionlessFilePath: string,
extensions: string[],
): Promise<string | null> => {
const directory: string = path.dirname(extensionlessFilePath);
try {
const matchingFiles = fs.readdirSync(directory).filter((file: string) => {
return (
extensions.some((ext) =>
file.toLowerCase().endsWith(ext.toLowerCase()),
) &&
path
.basename(file, path.extname(file))
.toLowerCase()
.startsWith(path.basename(extensionlessFilePath).toLowerCase())
);
});
const files: string[] = [];
matchingFiles.forEach((file) => {
const fullPath = path.join(directory, file);
files.push(fullPath);
});
// Return the first matching file if needed
if (files.length > 0) {
return files[0];
}
} catch (error) {
log.error(`Failed to read directory ${directory}:`, error);
throw error;
}
return null;
};
const getFilePathWithoutExtension = (filePath: string): string => {
return path.join(
path.dirname(filePath),
path.basename(filePath, path.extname(filePath)),
);
};
export { findFileCaseInsensitive };

415
src/main/decoder/decoder.ts Normal file
View File

@@ -0,0 +1,415 @@
import { platform } from "@electron-toolkit/utils";
import { UUID } from "crypto";
import { Notification, shell } from "electron";
import log from "electron-log/main";
import fs from "fs";
import _ from "lodash";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
import client from "../graphql/graphql-client";
import {
INSERT_AVAILABLE_JOB_TYPED,
InsertAvailableJobResult,
QUERY_JOB_BY_CLM_NO_TYPED,
QUERY_VEHICLE_BY_VIN_TYPED,
QueryJobByClmNoResult,
VehicleQueryResult,
} from "../graphql/queries";
import store from "../store/store";
import DecodeAD1 from "./decode-ad1";
import { DecodedAd1 } from "./decode-ad1.interface";
import DecodeAD2 from "./decode-ad2";
import { DecodedAD2 } from "./decode-ad2.interface";
import DecodeEnv from "./decode-env";
import { DecodedEnv } from "./decode-env.interface";
import DecodeLin from "./decode-lin";
import { DecodedLin } from "./decode-lin.interface";
import DecodePfh from "./decode-pfh";
import { DecodedPfh } from "./decode-pfh.interface";
import DecodePfl from "./decode-pfl";
import { DecodedPfl } from "./decode-pfl.interface";
import DecodePfm from "./decode-pfm";
import { DecodedPfm } from "./decode-pfm.interface";
import DecodePfo from "./decode-pfo";
import { DecodedPfo } from "./decode-pfo.interface";
import DecodePfp from "./decode-pfp";
import { DecodedPfp } from "./decode-pfp.interface";
import DecodePft from "./decode-pft";
import { DecodedPft } from "./decode-pft.interface";
import DecodeStl from "./decode-stl";
import { DecodedStl } from "./decode-stl.interface";
import DecodeTtl from "./decode-ttl";
import { DecodedTtl } from "./decode-ttl.interface";
import DecodeVeh from "./decode-veh";
import { DecodedVeh } from "./decode-veh.interface";
import setAppProgressbar from "../util/setAppProgressBar";
import UploadEmsToS3 from "./emsbackup";
async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name,
);
log.debug("Importing Job", extensionlessFilePath);
try {
await WaitForAllFiles(extensionlessFilePath, requiredExtensions);
//The below all end up returning parts of the job object.
//Some of them return additional info - e.g. owner or vehicle record data at both the job and corresponding table level.
setAppProgressbar(0.1);
const env: DecodedEnv = await DecodeEnv(extensionlessFilePath);
setAppProgressbar(0.15);
const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath);
setAppProgressbar(0.2);
const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath);
setAppProgressbar(0.25);
const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath);
setAppProgressbar(0.3);
const lin: DecodedLin = await DecodeLin(extensionlessFilePath);
setAppProgressbar(0.35);
const pfh: DecodedPfh = await DecodePfh(extensionlessFilePath);
setAppProgressbar(0.4);
const pfl: DecodedPfl = await DecodePfl(extensionlessFilePath);
setAppProgressbar(0.45);
const pft: DecodedPft = await DecodePft(extensionlessFilePath);
setAppProgressbar(0.5);
const pfm: DecodedPfm = await DecodePfm(extensionlessFilePath);
setAppProgressbar(0.55);
const pfo: DecodedPfo = await DecodePfo(extensionlessFilePath); // TODO: This will be the `cieca_pfo` object
setAppProgressbar(0.6);
const stl: DecodedStl = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object
setAppProgressbar(0.65);
const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath);
setAppProgressbar(0.7);
const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath);
setAppProgressbar(0.75);
const jobObjectUncleaned: RawJobDataObject = {
...env,
...ad1,
...ad2,
...veh,
...lin,
...pfh,
...pfl,
...pft,
...pfm,
...pfo,
...stl,
...ttl,
...pfp,
shopid: store.get("app.bodyshop.id") as UUID,
};
// Replace owner information with claimant information if necessary
const jobObject = ReplaceOwnerInfoWithClaimant(jobObjectUncleaned);
setAppProgressbar(0.8);
if (import.meta.env.DEV) {
// Save jobObject to a timestamped JSON file
const timestamp = new Date()
.toISOString()
.replace(/:/g, "-")
.replace(/\..+/, "");
const fileName = `job_${timestamp}_${parsedFilePath.name}.json`;
const logsDir = path.join(process.cwd(), "logs");
// Create logs directory if it doesn't exist
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const filePath = path.join(logsDir, fileName);
fs.writeFileSync(filePath, JSON.stringify(jobObject, null, 2), "utf8");
log.info(`Job data saved to: ${filePath}`);
}
const newAvailableJob: AvailableJobSchema = {
uploaded_by: store.get("user.email"),
bodyshopid: store.get("app.bodyshop.id"),
cieca_id: jobObject.ciecaid,
est_data: jobObject,
ownr_name: `${jobObject.ownr_fn} ${jobObject.ownr_ln} ${jobObject.ownr_co_nm}`,
ins_co_nm: jobObject.ins_co_nm,
vehicle_info: `${jobObject.v_model_yr} ${jobObject.v_make_desc} ${jobObject.v_model_desc}`,
clm_no: jobObject.clm_no,
clm_amt: jobObject.clm_total,
// source_system: jobObject.source_system, //TODO: Add back source system if needed.
issupplement: false,
jobid: null,
};
setAppProgressbar(0.85);
const existingVehicleRecord: VehicleQueryResult = await client.request(
QUERY_VEHICLE_BY_VIN_TYPED,
{
vin: jobObject.v_vin,
},
);
if (existingVehicleRecord.vehicles.length > 0) {
delete newAvailableJob.est_data.vehicle;
newAvailableJob.est_data.vehicleid = existingVehicleRecord.vehicles[0].id;
}
console.log("Available Job record to upload;", newAvailableJob);
setAppProgressbar(0.95);
const existingJobRecord: QueryJobByClmNoResult = await client.request(
QUERY_JOB_BY_CLM_NO_TYPED,
{ clm_no: jobObject.clm_no },
);
if (existingJobRecord.jobs.length > 0) {
newAvailableJob.issupplement = true;
newAvailableJob.jobid = existingJobRecord.jobs[0].id;
}
const insertRecordResult: InsertAvailableJobResult = await client.request(
INSERT_AVAILABLE_JOB_TYPED,
{
jobInput: [newAvailableJob],
},
);
setAppProgressbar(-1);
const uploadNotification = new Notification({
title: "Job Imported",
//subtitle: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}`,
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}. Click to view.`,
actions: [{ text: "View Job", type: "button" }],
});
uploadNotification.on("click", () => {
shell.openExternal(
`${
store.get("app.isTest")
? import.meta.env.VITE_FE_URL_TEST
: import.meta.env.VITE_FE_URL
}/manage/available`,
);
});
uploadNotification.show();
log.debug("Job inserted", insertRecordResult);
UploadEmsToS3({
extensionlessFilePath,
bodyshopid: newAvailableJob.bodyshopid,
ciecaid: jobObject.ciecaid ?? "",
clm_no: jobObject.clm_no ?? "",
ownr_ln: jobObject.ownr_ln ?? "",
});
} catch (error) {
log.error("Error encountered while decoding job. ", errorTypeCheck(error));
const uploadNotificationFailure = new Notification({
title: "Job Upload Failure",
body: errorTypeCheck(error).message, //TODO: Remove after debug.
});
uploadNotificationFailure.show();
}
}
export default ImportJob;
export interface RawJobDataObject
extends DecodedEnv,
DecodedAd1,
DecodedAD2,
DecodedVeh,
DecodedLin,
DecodedPfh,
DecodedPfl,
DecodedPft,
DecodedPfm,
DecodedPfo,
DecodedStl,
DecodedTtl,
DecodedPfp {
vehicleid?: UUID;
shopid: UUID;
}
export interface AvailableJobSchema {
uploaded_by: string;
bodyshopid: UUID;
cieca_id?: string;
est_data: RawJobDataObject;
ownr_name: string;
ins_co_nm?: string;
vehicle_info: string;
clm_no?: string;
clm_amt: number;
source_system?: string | null;
issupplement: boolean;
jobid: UUID | null;
}
async function WaitForAllFiles(
baseFilePath: string,
requiredExtensions: string[],
maxRetries: number = 5,
backoffMs: number = 1000,
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
//Get all files in directory if Mac.
let filesInDir: string[] = [];
if (platform.isMacOS) {
const dir: string = path.dirname(baseFilePath);
filesInDir = fs.readdirSync(dir).map((file) => file.toLowerCase());
}
const missingFiles = requiredExtensions.filter((ext) => {
const filePath: string = `${baseFilePath}.${ext}`;
const filePathA: string = `${baseFilePath}A.${ext}`;
const filePathB: string = `${baseFilePath}B.${ext}`;
const filePathV: string = `${baseFilePath}V.${ext}`;
if (!platform.isWindows) {
// Case-insensitive check for macOS/Linux
const baseName: string = path.basename(baseFilePath);
return !(
filesInDir.includes(`${baseName}.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}A.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}B.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}V.${ext}`.toLowerCase())
);
} else {
// Case-sensitive check for other platforms
return !(
fs.existsSync(filePath) ||
fs.existsSync(filePathA) ||
fs.existsSync(filePathB) ||
fs.existsSync(filePathV)
);
}
});
if (missingFiles.length === 0) {
return; // All files are present
}
log.debug(
`Attempt ${attempt}: Missing files: ${missingFiles.join(", ")}. Retrying in ${backoffMs}ms...`,
);
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, backoffMs));
backoffMs *= 2; // Exponential backoff
} else {
throw new Error(
`The set of files is not valid. Missing files for CIECA ID ${baseFilePath}: ${missingFiles.join(", ")}`,
);
}
}
}
const requiredExtensions = [
"env",
"ad1",
"ad2",
"veh",
"lin",
"pfh",
"pfl",
"pft",
"pfm",
"pfo",
"stl",
"ttl",
"pfp",
];
export function ReplaceOwnerInfoWithClaimant<
T extends Partial<
Pick<
RawJobDataObject,
| "ownr_ln"
| "ownr_fn"
| "ownr_co_nm"
| "ownr_title"
| "ownr_co_nm"
| "ownr_addr1"
| "ownr_addr2"
| "ownr_city"
| "ownr_st"
| "ownr_zip"
| "ownr_ctry"
| "ownr_ph1"
| "ownr_ph2"
| "ownr_ea"
| "clmt_ln"
| "clmt_fn"
| "clmt_title"
| "clmt_co_nm"
| "clmt_addr1"
| "clmt_addr2"
| "clmt_city"
| "clmt_st"
| "clmt_zip"
| "clmt_ctry"
| "clmt_ph1"
| "clmt_ph2"
| "clmt_ea"
| "owner"
>
>,
>(jobObject: T): T {
// In some scenarios, the owner information is missing. So we use the claimant instead.
// We pull the claimant info for this, but we don't store it in our system, so it needs to be deleted regardless.
if (
_.isEmpty(jobObject.ownr_ln) &&
_.isEmpty(jobObject.ownr_fn) &&
_.isEmpty(jobObject.ownr_co_nm)
) {
jobObject.ownr_ln = jobObject.clmt_ln;
jobObject.ownr_fn = jobObject.clmt_fn;
jobObject.ownr_title = jobObject.clmt_title;
jobObject.ownr_co_nm = jobObject.clmt_co_nm;
jobObject.ownr_addr1 = jobObject.clmt_addr1;
jobObject.ownr_addr2 = jobObject.clmt_addr2;
jobObject.ownr_city = jobObject.clmt_city;
jobObject.ownr_st = jobObject.clmt_st;
jobObject.ownr_zip = jobObject.clmt_zip;
jobObject.ownr_ctry = jobObject.clmt_ctry;
jobObject.ownr_ph1 = jobObject.clmt_ph1;
jobObject.ownr_ph2 = jobObject.clmt_ph2;
jobObject.ownr_ea = jobObject.clmt_ea;
// Ensure the owner and owner.data fields exist before assigning values
if (jobObject.owner?.data) {
jobObject.owner.data.ownr_ln = jobObject.clmt_ln;
jobObject.owner.data.ownr_fn = jobObject.clmt_fn;
jobObject.owner.data.ownr_title = jobObject.clmt_title;
jobObject.owner.data.ownr_co_nm = jobObject.clmt_co_nm;
jobObject.owner.data.ownr_addr1 = jobObject.clmt_addr1;
jobObject.owner.data.ownr_addr2 = jobObject.clmt_addr2;
jobObject.owner.data.ownr_city = jobObject.clmt_city;
jobObject.owner.data.ownr_st = jobObject.clmt_st;
jobObject.owner.data.ownr_zip = jobObject.clmt_zip;
jobObject.owner.data.ownr_ctry = jobObject.clmt_ctry;
jobObject.owner.data.ownr_ph1 = jobObject.clmt_ph1;
jobObject.owner.data.ownr_ph2 = jobObject.clmt_ph2;
jobObject.owner.data.ownr_ea = jobObject.clmt_ea;
}
}
// Delete the claimant info as it's not needed.
delete jobObject.clmt_ln;
delete jobObject.clmt_fn;
delete jobObject.clmt_title;
delete jobObject.clmt_co_nm;
delete jobObject.clmt_addr1;
delete jobObject.clmt_addr2;
delete jobObject.clmt_city;
delete jobObject.clmt_st;
delete jobObject.clmt_zip;
delete jobObject.clmt_ctry;
delete jobObject.clmt_ph1;
delete jobObject.clmt_ph2;
delete jobObject.clmt_ea;
return jobObject;
}

View File

@@ -0,0 +1,104 @@
import axios from "axios";
import archiver from "archiver";
import errorTypeCheck from "../../util/errorTypeCheck";
import { UUID } from "crypto";
import fs from "fs";
import path from "path";
import stream from "stream";
import { getTokenFromRenderer } from "../graphql/graphql-client";
import store from "../store/store";
async function UploadEmsToS3({
extensionlessFilePath,
bodyshopid,
clm_no,
ciecaid,
ownr_ln,
}: {
extensionlessFilePath: string;
bodyshopid: UUID;
clm_no: string;
ciecaid: string;
ownr_ln: string;
}): Promise<boolean> {
// This function is a placeholder for the actual upload logic
try {
const directory = path.dirname(extensionlessFilePath);
const baseFilename = path.basename(extensionlessFilePath);
// Find all files in the directory that start with the base filename
const filesToZip = fs
.readdirSync(directory)
.filter((file) => file.startsWith(baseFilename))
.map((file) => path.join(directory, file));
if (filesToZip.length === 0) {
console.error("No files found to zip.");
return false;
}
// Create a zip archive in memory
const archive = archiver("zip", { zlib: { level: 9 } });
const zipBuffer = await new Promise<Buffer>((resolve, reject) => {
const buffers: Buffer[] = [];
const writableStream = new stream.Writable({
write(chunk, _encoding, callback) {
buffers.push(chunk);
callback();
},
});
writableStream.on("finish", () => resolve(Buffer.concat(buffers)));
writableStream.on("error", reject);
archive.pipe(writableStream);
// Append files to the archive
filesToZip.forEach((file) => {
archive.file(file, { name: path.basename(file) });
});
archive.finalize();
});
// Get the presigned URL from the server
const presignedUrlResponse = await axios.post(
`${
store.get("app.isTest")
? import.meta.env.VITE_API_TEST_URL
: import.meta.env.VITE_API_URL
}/emsupload`,
{
bodyshopid,
ciecaid,
clm_no,
ownr_ln,
},
{
headers: {
Authorization: `Bearer ${await getTokenFromRenderer()}`,
},
},
);
const presignedUrl = presignedUrlResponse.data?.presignedUrl;
if (!presignedUrl) {
console.error("Failed to retrieve presigned URL.");
return false;
}
// Upload the zip file to S3 using the presigned URL
await axios.put(presignedUrl, zipBuffer, {
headers: {
"Content-Type": "application/zip",
},
});
} catch (error) {
console.error("Error uploading EMS to S3:", errorTypeCheck(error));
return false;
}
return true; // Return true if the upload is successful
}
export default UploadEmsToS3;

View File

@@ -0,0 +1,57 @@
import path from "path";
import { GetAllEnvFiles } from "../watcher/watcher";
import DecodeAD1 from "./decode-ad1";
import DecodeAD2 from "./decode-ad2";
import DecodeEnv from "./decode-env";
import DecodeVeh from "./decode-veh";
import { ReplaceOwnerInfoWithClaimant } from "./decoder";
const folderScan = async (): Promise<FolderScanResult[]> => {
//Get all ENV files for watched paths.
const allEnvFiles = GetAllEnvFiles();
//Run a simplified decode on them
const returnedFiles: FolderScanResult[] = [];
for (const filepath of allEnvFiles) {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name,
);
const rawJob = {
...(await DecodeEnv(extensionlessFilePath)),
...(await DecodeAD1(extensionlessFilePath)),
...(await DecodeAD2(extensionlessFilePath)),
...(await DecodeVeh(extensionlessFilePath)),
};
const job = ReplaceOwnerInfoWithClaimant(rawJob);
const scanResult: FolderScanResult = {
id: job.ciecaid,
filepath: filepath,
cieca_id: job.ciecaid,
clm_no: job.clm_no,
owner: `${job.ownr_fn} ${job.ownr_ln} ${job.ownr_co_nm}`.trim(),
vehicle:
`${job.vehicle?.data.v_model_yr} ${job.vehicle?.data.v_make_desc} ${job.vehicle?.data.v_model_desc}`.trim(),
ins_co_nm: job.ins_co_nm,
};
returnedFiles.push(scanResult);
}
//Build up the object and return it
return returnedFiles;
};
export interface FolderScanResult {
id?: string;
filepath: string;
cieca_id?: string;
clm_no?: string;
owner: string;
ins_co_nm?: string;
vehicle: string;
}
export default folderScan;

View File

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

View File

@@ -0,0 +1,158 @@
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 = [
{
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}.AD2`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD2`),
ad2FieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} AD2 file records added.`);
return true;
} catch (error) {
console.error("Error generating AD2 file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateAd2File;

View File

@@ -0,0 +1,47 @@
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 records = [
{
EST_SYSTEM: "C",
RO_ID: partsOrder.job.ro_number,
ESTFILE_ID: partsOrder.job.ciecaid,
STATUS: false,
INCL_ADMIN: true,
INCL_VEH: true,
INCL_EST: true,
INCL_PROFL: true,
INCL_TOTAL: true,
INCL_VENDR: false,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.ENV`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.ENV`),
envFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} ENV file records added.`);
return true;
} catch (error) {
console.error("Error generating ENV file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateEnvFile;

View File

@@ -0,0 +1,87 @@
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,
WHO_PAYS: partsOrderLine.jobline?.who_pays,
PART_DESCJ: partsOrderLine.jobline?.part_descj,
LINE_DESC: partsOrderLine.jobline?.line_desc,
PART_TYPE:
partsOrderLine.priceChange === true
? partsOrderLine.part_type
: partsOrderLine.jobline?.part_type,
GLASS_FLAG: partsOrderLine.jobline?.glass_flag,
OEM_PARTNO: partsOrderLine.jobline?.oem_partno,
PRICE_INC: partsOrderLine.jobline?.price_inc,
ALT_PART_I: partsOrderLine.jobline?.alt_part_i,
TAX_PART: partsOrderLine.jobline?.tax_part,
DB_PRICE: partsOrderLine.jobline?.db_price,
ACT_PRICE:
partsOrderLine.priceChange === true
? partsOrderLine.act_price
: partsOrderLine.jobline?.act_price,
PRICE_J: partsOrderLine.jobline?.price_j,
CERT_PART: partsOrderLine.jobline?.cert_part,
PART_QTY: partsOrderLine.jobline?.part_qty,
ALT_CO_ID: partsOrderLine.jobline?.alt_co_id,
ALT_PARTNO: partsOrderLine.jobline?.alt_partno,
ALT_OVERRD: partsOrderLine.jobline?.alt_overrd,
ALT_PARTM: partsOrderLine.jobline?.alt_partm,
PRT_DSMK_P: partsOrderLine.jobline?.prt_dsmk_p,
PRT_DSMK_M: partsOrderLine.jobline?.prt_dsmk_m,
MOD_LBR_TY: partsOrderLine.jobline?.mod_lbr_ty,
DB_HRS: partsOrderLine.jobline?.db_hrs,
MOD_LB_HRS: partsOrderLine.jobline?.mod_lb_hrs,
LBR_INC: partsOrderLine.jobline?.lbr_inc,
LBR_OP: partsOrderLine.jobline?.lbr_op,
LBR_HRS_J: partsOrderLine.jobline?.lbr_hrs_j,
LBR_TYP_J: partsOrderLine.jobline?.lbr_typ_j,
LBR_OP_J: partsOrderLine.jobline?.lbr_op_j,
PAINT_STG: partsOrderLine.jobline?.paint_stg,
PAINT_TONE: partsOrderLine.jobline?.paint_tone,
LBR_TAX: partsOrderLine.jobline?.lbr_tax,
LBR_AMT: partsOrderLine.jobline?.lbr_amt,
MISC_AMT: partsOrderLine.jobline?.misc_amt,
MISC_SUBLT: partsOrderLine.jobline?.misc_sublt,
MISC_TAX: partsOrderLine.jobline?.misc_tax,
BETT_TYPE: partsOrderLine.jobline?.bett_type,
BETT_PCTG: partsOrderLine.jobline?.bett_pctg,
BETT_AMT: partsOrderLine.jobline?.bett_amt,
BETT_TAX: partsOrderLine.jobline?.bett_tax,
}));
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.LIN`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.LIN`),
linFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} LIN file records added.`);
return true;
} catch (error) {
console.error("Error generating LIN file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateLinFile;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
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 };

Some files were not shown because too many files have changed in this diff Show More