diff --git a/.env.imex b/.env.imex
index 1d6ab25..3bd811e 100644
--- a/.env.imex
+++ b/.env.imex
@@ -1,7 +1,14 @@
-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_GRAPHQL_ENDPOINT=https://db.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
+
+# 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
\ No newline at end of file
+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"
\ No newline at end of file
diff --git a/.env.local b/.env.local
index 5ca67ff..b364357 100644
--- a/.env.local
+++ b/.env.local
@@ -4,4 +4,6 @@ VITE_FIREBASE_CONFIG_TEST={ "apiKey":"AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c"
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
\ No newline at end of file
+VITE_FE_URL_TEST=https://test.imex.online
+VITE_API_URL="http://localhost:4000"
+VITE_API_TEST_URL="http://api.test.imex.online"
\ No newline at end of file
diff --git a/.env.production b/.env.production
deleted file mode 100644
index f7dd5a7..0000000
--- a/.env.production
+++ /dev/null
@@ -1,2 +0,0 @@
-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_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
\ No newline at end of file
diff --git a/.env.rome b/.env.rome
index 4180038..9695644 100644
--- a/.env.rome
+++ b/.env.rome
@@ -1,7 +1,14 @@
-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_GRAPHQL_ENDPOINT=https://db.romeonline.io/v1/graphql
-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"}
-VITE_GRAPHQL_ENDPOINT_TEST=https://db.test.romeonline.io/v1/graphql
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
\ No newline at end of file
+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"
\ No newline at end of file
diff --git a/.env.staging b/.env.staging
deleted file mode 100644
index e69de29..0000000
diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml
index 0cb5647..4647599 100644
--- a/.idea/material_theme_project_new.xml
+++ b/.idea/material_theme_project_new.xml
@@ -3,7 +3,9 @@
diff --git a/electron-builder.imex.yml b/electron-builder.imex.yml
index 3d85ad6..5fde59c 100644
--- a/electron-builder.imex.yml
+++ b/electron-builder.imex.yml
@@ -7,6 +7,7 @@ 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}"
@@ -26,6 +27,7 @@ nsis:
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
@@ -34,6 +36,11 @@ mac:
- 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:
@@ -50,12 +57,11 @@ linux:
- deb
maintainer: electronjs.org
category: Utility
+ desktop: scripts/imex-shop-partner.desktop
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: s3
bucket: imex-partner
- region: ca-central-1
-# electronDownload:
-# mirror: https://npmmirror.com/mirrors/electron/
+ region: ca-central-1
\ No newline at end of file
diff --git a/electron-builder.rome.yml b/electron-builder.rome.yml
index 01ede46..b01d3cd 100644
--- a/electron-builder.rome.yml
+++ b/electron-builder.rome.yml
@@ -7,6 +7,7 @@ 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}"
@@ -26,6 +27,7 @@ nsis:
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
@@ -34,6 +36,11 @@ mac:
- 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:
@@ -50,12 +57,11 @@ linux:
- deb
maintainer: electronjs.org
category: Utility
+ desktop: scripts/rome-shop-partner.desktop
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: s3
bucket: rome-partner
- region: us-east-2
-# electronDownload:
-# mirror: https://npmmirror.com/mirrors/electron/
+ region: us-east-2
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 758e375..745f7c1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "bodyshop-desktop",
- "version": "0.0.1-alpha.6",
+ "version": "0.0.1-alpha.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bodyshop-desktop",
- "version": "0.0.1-alpha.6",
+ "version": "0.0.1-alpha.8",
"hasInstallScript": true,
"dependencies": {
"@apollo/client": "^3.13.6",
@@ -14,6 +14,7 @@
"@electron-toolkit/utils": "^4.0.0",
"@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",
@@ -31,8 +32,10 @@
"@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",
"antd": "^5.24.6",
"chokidar": "^4.0.3",
@@ -52,6 +55,7 @@
"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.1.0",
@@ -62,7 +66,9 @@
"react-router": "^7.5.0",
"redux-logger": "^3.0.6",
"typescript": "^5.8.3",
- "vite": "6.2.6"
+ "vite": "6.2.6",
+ "xml2js": "^0.6.2",
+ "xmlbuilder2": "^3.1.1"
}
},
"node_modules/@ampproject/remapping": {
@@ -2695,6 +2701,58 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
+ "node_modules/@oozcitak/dom": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz",
+ "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/infra": "1.0.8",
+ "@oozcitak/url": "1.0.4",
+ "@oozcitak/util": "8.3.8"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/@oozcitak/infra": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz",
+ "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/util": "8.3.8"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/@oozcitak/url": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz",
+ "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/infra": "1.0.8",
+ "@oozcitak/util": "8.3.8"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/@oozcitak/util": {
+ "version": "8.3.8",
+ "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz",
+ "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -4322,13 +4380,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
@@ -4459,6 +4510,13 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/node-cron": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz",
+ "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/pg": {
"version": "8.6.1",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
@@ -4587,6 +4645,16 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/xml2js": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz",
+ "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -5627,7 +5695,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@@ -5666,6 +5733,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axios": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
+ "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -6138,7 +6216,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -6399,7 +6476,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@@ -6943,7 +7019,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -7189,7 +7264,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -7706,7 +7780,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7716,7 +7789,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7754,7 +7826,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -7767,7 +7838,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -8144,6 +8214,20 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
@@ -8528,6 +8612,26 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -8578,7 +8682,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
- "dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@@ -8594,7 +8697,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -8604,7 +8706,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -8798,7 +8899,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -8823,7 +8923,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -8977,7 +9076,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -9117,7 +9215,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -9130,7 +9227,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -10561,7 +10657,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -11026,6 +11121,19 @@
"node": ">=10"
}
},
+ "node_modules/node-cron": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "uuid": "8.3.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -12851,13 +12959,12 @@
}
},
"node_modules/react-router": {
- "version": "7.5.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz",
- "integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz",
+ "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
@@ -14773,6 +14880,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -15149,6 +15266,30 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xml2js/node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
@@ -15159,6 +15300,53 @@
"node": ">=8.0"
}
},
+ "node_modules/xmlbuilder2": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz",
+ "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/dom": "1.15.10",
+ "@oozcitak/infra": "1.0.8",
+ "@oozcitak/util": "8.3.8",
+ "js-yaml": "3.14.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
+ "node_modules/xmlbuilder2/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/xmlbuilder2/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/xmlbuilder2/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/package.json b/package.json
index f751d16..315ceec 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "bodyshop-desktop",
- "version": "0.0.1-alpha.7",
+ "version": "0.0.1-alpha.8",
"description": "Shop Management System Partner",
"main": "./out/main/index.js",
"author": "Convenient Brands, LLC",
@@ -17,11 +17,13 @@
"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": "npm run build && electron-builder --mac",
- "build:linux": "npm run 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",
@@ -29,6 +31,7 @@
"@electron-toolkit/utils": "^4.0.0",
"@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",
@@ -46,8 +49,10 @@
"@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",
"antd": "^5.24.6",
"chokidar": "^4.0.3",
@@ -77,6 +82,9 @@
"react-router": "^7.5.0",
"redux-logger": "^3.0.6",
"typescript": "^5.8.3",
- "vite": "6.2.6"
+ "vite": "6.2.6",
+ "xml2js": "^0.6.2",
+ "xmlbuilder2": "^3.1.1",
+ "node-cron": "^3.0.3"
}
}
diff --git a/scripts/imex-shop-partner.desktop b/scripts/imex-shop-partner.desktop
new file mode 100644
index 0000000..1085305
--- /dev/null
+++ b/scripts/imex-shop-partner.desktop
@@ -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;
\ No newline at end of file
diff --git a/scripts/installer.nsh b/scripts/installer.nsh
new file mode 100644
index 0000000..0333f74
--- /dev/null
+++ b/scripts/installer.nsh
@@ -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
\ No newline at end of file
diff --git a/scripts/rome-shop-partner.desktop b/scripts/rome-shop-partner.desktop
new file mode 100644
index 0000000..5fe6a89
--- /dev/null
+++ b/scripts/rome-shop-partner.desktop
@@ -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;
\ No newline at end of file
diff --git a/src/main/index.ts b/src/main/index.ts
index 100fb5d..d60d8e3 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -25,6 +25,14 @@ import store from "./store/store";
import { checkForAppUpdates } from "./util/checkForAppUpdates";
import { getMainWindow } from "./util/toRenderer";
import { GetAllEnvFiles } from "./watcher/watcher";
+import {
+ isKeepAliveAgentInstalled,
+ setupKeepAliveAgent,
+} from "./setup-keep-alive-agent";
+import {
+ isKeepAliveTaskInstalled,
+ setupKeepAliveTask,
+} from "./setup-keep-alive-task";
Sentry.init({
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
@@ -34,6 +42,7 @@ log.initialize();
const isMac: boolean = process.platform === "darwin";
const protocol: string = "imexmedia";
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
+let isKeepAliveLaunch = false; // Track if launched via keep-alive
// Initialize the server
const localServer = new LocalServer();
const gotTheLock = app.requestSingleInstanceLock();
@@ -52,7 +61,7 @@ function createWindow(): void {
height,
x,
y,
- show: false,
+ show: false, // Start hidden, show later if not keep-alive
minWidth: 600,
minHeight: 400,
//autoHideMenuBar: true,
@@ -192,7 +201,14 @@ function createWindow(): void {
label: "Open Log File",
click: (): void => {
/* action for item 1 */
- shell.openPath(log.transports.file.getFile().path);
+ shell
+ .openPath(log.transports.file.getFile().path)
+ .catch((error) => {
+ log.error(
+ "Failed to open log file:",
+ errorTypeCheck(error),
+ );
+ });
},
},
{
@@ -204,7 +220,12 @@ function createWindow(): void {
{
label: "Open Config Folder",
click: (): void => {
- shell.openPath(path.dirname(store.path));
+ shell.openPath(path.dirname(store.path)).catch((error) => {
+ log.error(
+ "Failed to open config folder:",
+ errorTypeCheck(error),
+ );
+ });
},
},
{
@@ -218,13 +239,38 @@ function createWindow(): void {
{
type: "separator",
},
-
// {
// label: "Decode Hardcoded Estimate",
// click: (): void => {
// ImportJob(`C:\\EMS\\CCC\\9ee762f4.ENV`);
// },
// },
+ {
+ label: "Install Keep Alive",
+ enabled: true, // Default to enabled, update dynamically
+ click: async (): Promise => {
+ try {
+ if (platform.isWindows) {
+ log.debug("Creating Windows keep-alive task");
+ await setupKeepAliveTask();
+ log.info("Successfully installed Windows keep-alive task");
+ } else if (platform.isMacOS) {
+ log.debug("Creating macOS keep-alive agent");
+ await setupKeepAliveAgent();
+ log.info("Successfully installed macOS keep-alive agent");
+ }
+ // Wait to ensure task/agent is registered
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+ // Rebuild menu and update enabled state
+ await updateKeepAliveMenuItem();
+ } catch (error) {
+ log.error(
+ `Failed to install keep-alive: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ // Optionally notify user (e.g., via dialog or log)
+ }
+ },
+ },
{
label: "Add All Estimates in watched directories",
click: (): void => {
@@ -254,8 +300,45 @@ function createWindow(): void {
},
];
+ // Dynamically update Install Keep Alive enabled state
+ const updateKeepAliveMenuItem = async (): Promise => {
+ try {
+ const isInstalled = platform.isWindows
+ ? await isKeepAliveTaskInstalled()
+ : platform.isMacOS
+ ? await isKeepAliveAgentInstalled()
+ : false;
+ const developmentMenu = template
+ .find((item) => item.label === "Application")
+ // @ts-ignore
+ ?.submenu?.find((item: { id: string }) => item.id === "development")
+ ?.submenu as Electron.MenuItemConstructorOptions[];
+ const keepAliveItem = developmentMenu?.find(
+ (item) => item.label === "Install Keep Alive",
+ );
+ if (keepAliveItem) {
+ keepAliveItem.enabled = !isInstalled; // Enable if not installed, disable if installed
+ const menu = Menu.buildFromTemplate(template);
+ Menu.setApplicationMenu(menu);
+ log.debug(
+ `Updated Install Keep Alive menu item: enabled=${keepAliveItem.enabled}`,
+ );
+ }
+ } catch (error) {
+ log.error(
+ `Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ };
+
const menu: Electron.Menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
+ // Update menu item enabled state on app start
+ updateKeepAliveMenuItem().catch((error) => {
+ log.error(
+ `Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ });
// Register a global shortcut to show the hidden item
globalShortcut.register("CommandOrControl+Shift+T", () => {
@@ -266,7 +349,7 @@ function createWindow(): void {
const fileMenu = template.find((item) => item.label === "Application");
// @ts-ignore
const hiddenItem = fileMenu?.submenu?.find(
- (item) => item.id === "development",
+ (item: { id: string }) => item.id === "development",
);
//Adjust the development menu as well.
@@ -289,7 +372,9 @@ function createWindow(): void {
mainWindow.on("moved", storeWindowState);
mainWindow.on("ready-to-show", () => {
- mainWindow.show();
+ if (!isKeepAliveLaunch) {
+ mainWindow.show(); // Show only if not a keep-alive launch
+ }
//Start the HTTP server.
// Start the local HTTP server
try {
@@ -307,16 +392,24 @@ function createWindow(): void {
});
mainWindow.webContents.setWindowOpenHandler((details) => {
- shell.openExternal(details.url);
+ shell.openExternal(details.url).catch((error) => {
+ log.error("Failed to open external URL:", errorTypeCheck(error));
+ });
return { action: "deny" };
});
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
- mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
+ mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]).catch((error) => {
+ log.error("Failed to load URL:", errorTypeCheck(error));
+ });
} else {
- mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
+ mainWindow
+ .loadFile(join(__dirname, "../renderer/index.html"))
+ .catch((error) => {
+ log.error("Failed to load file:", errorTypeCheck(error));
+ });
}
if (import.meta.env.DEV) {
mainWindow.webContents.openDevTools();
@@ -341,7 +434,8 @@ app.whenReady().then(async () => {
optimizer.watchWindowShortcuts(window);
});
- let isDefaultProtocolClient;
+ let isDefaultProtocolClient: boolean;
+
// remove so we can register each time as we run the app.
app.removeAsDefaultProtocolClient(protocol);
@@ -365,25 +459,30 @@ app.whenReady().then(async () => {
// Add this event handler for second instance
app.on("second-instance", (_event: Electron.Event, argv: string[]) => {
- // Someone tried to run a second instance, we should focus our window
- openMainWindow();
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
if (url) {
- openInExplorer(url);
+ if (url.startsWith(`${protocol}://keep-alive`)) {
+ log.info("Keep-alive protocol received, app is already running.");
+ // Do nothing if already running
+ return; // Skip openMainWindow to avoid focusing the window
+ } else {
+ openInExplorer(url);
+ }
+ } else {
+ openMainWindow(); // Focus window if no URL
}
});
//Dynamically load ipcMain handlers once ready.
try {
- // Replace 'path/to/your/file' with the actual path to your file
const module = await import("./ipc/ipcMainConfig");
+ log.debug("Successfully loaded ipcMainConfig");
- // You can now use anything exported from the module
- // For example:
- // module.someFunction()
- log.debug("Successfully loaded ipcMainConfig", module);
+ // Initialize cron tasks after loading ipcMainConfig
+ await module.initializeCronTasks();
+ log.info("Cron tasks initialized successfully");
} catch (error) {
- log.error("Failed to load ipcMainConfig", {
+ log.error("Failed to load ipcMainConfig or initialize cron tasks", {
...ErrorTypeCheck(error),
});
}
@@ -453,6 +552,12 @@ app.whenReady().then(async () => {
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.downloaded, info);
});
+ // Check if launched with keep-alive protocol (Windows)
+ const args = process.argv.slice(1);
+ if (args.some((arg) => arg.startsWith(`${protocol}://keep-alive`))) {
+ isKeepAliveLaunch = true;
+ }
+
//The update itself will run when the bodyshop record is queried to know what release channel to use.
createWindow();
@@ -464,7 +569,17 @@ app.whenReady().then(async () => {
app.on("open-url", (event: Electron.Event, url: string) => {
event.preventDefault();
//Don't do anything for now. We just want to open the app.
- openInExplorer(url);
+ if (url.startsWith(`${protocol}://keep-alive`)) {
+ log.info("Keep-alive protocol received.");
+ if (BrowserWindow.getAllWindows().length === 0) {
+ isKeepAliveLaunch = true;
+ openMainWindow(); // Launch app if not running
+ }
+ // Do nothing if already running
+ return; // Skip openMainWindow to avoid focusing the window
+ } else {
+ openInExplorer(url);
+ }
});
// Quit when all windows are closed, except on macOS. There, it's common
@@ -518,5 +633,7 @@ function openMainWindow(): void {
function openInExplorer(url: string): void {
const folderPath: string = decodeURIComponent(url.split(`${protocol}://`)[1]);
log.info("Opening folder in explorer", folderPath);
- shell.openPath(folderPath);
+ shell.openPath(folderPath).catch((error) => {
+ log.error("Failed to open folder in explorer:", errorTypeCheck(error));
+ });
}
diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts
index 7a6f3ad..fba4f99 100644
--- a/src/main/ipc/ipcMainConfig.ts
+++ b/src/main/ipc/ipcMainConfig.ts
@@ -7,6 +7,14 @@ import ImportJob from "../decoder/decoder";
import store from "../store/store";
import { StartWatcher, StopWatcher } from "../watcher/watcher";
import {
+ SettingEmsOutFilePathGet,
+ SettingEmsOutFilePathSet,
+ SettingsPaintScaleInputConfigsGet,
+ SettingsPaintScaleInputConfigsSet,
+ SettingsPaintScaleInputPathSet,
+ SettingsPaintScaleOutputConfigsGet,
+ SettingsPaintScaleOutputConfigsSet,
+ SettingsPaintScaleOutputPathSet,
SettingsPpcFilePathGet,
SettingsPpcFilePathSet,
SettingsWatchedFilePathsAdd,
@@ -14,22 +22,38 @@ import {
SettingsWatchedFilePathsRemove,
SettingsWatcherPollingGet,
SettingsWatcherPollingSet,
- SettingEmsOutFilePathSet,
- SettingEmsOutFilePathGet,
} from "./ipcMainHandler.settings";
import {
ipcMainHandleAuthStateChanged,
ipMainHandleResetPassword,
} from "./ipcMainHandler.user";
+import cron from "node-cron";
+import { PaintScaleConfig, PaintScaleType } from "../../util/types/paintScale";
+import { ppgInputHandler, ppgOutputHandler } from "./paintScaleHandlers/PPG";
+
+const initializeCronTasks = async () => {
+ try {
+ // Fetch input and output configurations
+ const inputConfigs = await SettingsPaintScaleInputConfigsGet();
+ const outputConfigs = await SettingsPaintScaleOutputConfigsGet();
+
+ // Start input cron tasks
+ await handlePaintScaleInputCron(inputConfigs);
+ log.info("Initialized input cron tasks on app startup");
+
+ // Start output cron tasks
+ await handlePaintScaleOutputCron(outputConfigs);
+ log.info("Initialized output cron tasks on app startup");
+ } catch (error) {
+ log.error("Error initializing cron tasks on app startup:", error);
+ }
+};
// Log all IPC messages and their payloads
const logIpcMessages = (): void => {
- // Get all message types from ipcTypes.toMain
Object.keys(ipcTypes.toMain).forEach((key) => {
const messageType = ipcTypes.toMain[key];
-
- // Wrap the original handler with our logging
- const originalHandler = ipcMain.listeners(messageType)[0];
+ const originalHandler = ipcMain.listeners(messageType)?.[0];
if (originalHandler) {
ipcMain.removeAllListeners(messageType);
}
@@ -40,7 +64,6 @@ const logIpcMessages = (): void => {
"color: green",
payload,
);
- // Call original handler if it existed
if (originalHandler) {
originalHandler(event, payload);
}
@@ -48,21 +71,91 @@ const logIpcMessages = (): void => {
});
};
+// Input handler map
+const inputTypeHandlers: Partial<
+ Record Promise>
+> = {
+ [PaintScaleType.PPG]: ppgInputHandler,
+ // Add other input type handlers as needed
+};
+
+// Output handler map
+const outputTypeHandlers: Partial<
+ Record Promise>
+> = {
+ [PaintScaleType.PPG]: ppgOutputHandler,
+ // Add other output type handlers as needed
+};
+
+// Default handler for unsupported types
+const defaultHandler = async (config: PaintScaleConfig) => {
+ log.debug(
+ `No handler defined for type ${config.type} in config ${config.id}`,
+ );
+};
+
+// Input cron job management
+let inputCronTasks: { [id: string]: cron.ScheduledTask } = {};
+
+const handlePaintScaleInputCron = async (configs: PaintScaleConfig[]) => {
+ Object.values(inputCronTasks).forEach((task) => task.stop());
+ inputCronTasks = {};
+
+ const validConfigs = configs.filter(
+ (config) => config.path && config.path.trim() !== "",
+ );
+
+ validConfigs.forEach((config) => {
+ const cronExpression = `*/${config.pollingInterval} * * * *`;
+ inputCronTasks[config.id] = cron.schedule(cronExpression, async () => {
+ const handler = inputTypeHandlers[config.type] || defaultHandler;
+ await handler(config);
+ });
+ log.info(
+ `Started input cron task for config ${config.id} (type: ${config.type}) with interval ${config.pollingInterval}m`,
+ );
+ });
+};
+
+// Output cron job management
+let outputCronTasks: { [id: string]: cron.ScheduledTask } = {};
+
+const handlePaintScaleOutputCron = async (configs: PaintScaleConfig[]) => {
+ Object.values(outputCronTasks).forEach((task) => task.stop());
+ outputCronTasks = {};
+
+ const validConfigs = configs.filter(
+ (config) => config.path && config.path.trim() !== "",
+ );
+
+ validConfigs.forEach((config) => {
+ const cronExpression = `*/${config.pollingInterval} * * * *`;
+ outputCronTasks[config.id] = cron.schedule(cronExpression, async () => {
+ const handler = outputTypeHandlers[config.type] || defaultHandler;
+ await handler(config);
+ });
+ log.info(
+ `Started output cron task for config ${config.id} (type: ${config.type}) with interval ${config.pollingInterval}m`,
+ );
+ });
+};
+
+// Existing IPC handlers...
+
ipcMain.on(ipcTypes.toMain.test, () =>
console.log("** Verify that ipcMain is loaded and working."),
);
-//Auth handler
+// Auth handler
ipcMain.on(ipcTypes.toMain.authStateChanged, ipcMainHandleAuthStateChanged);
ipcMain.on(ipcTypes.toMain.user.resetPassword, ipMainHandleResetPassword);
-//Add debug handlers if in development
+// Add debug handlers if in development
if (import.meta.env.DEV) {
log.debug("[IPC Debug Functions] Adding Debug Handlers");
ipcMain.on(ipcTypes.toMain.debug.decodeEstimate, async (): Promise => {
const relativeEmsFilepath = `_reference/ems/MPI_1/3698420.ENV`;
- // Get the app's root directory and create an absolute path
const rootDir = app.getAppPath();
const absoluteFilepath = path.join(rootDir, relativeEmsFilepath);
@@ -76,7 +169,7 @@ if (import.meta.env.DEV) {
});
}
-//Settings Handlers
+// Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.get,
SettingsWatchedFilePathsGet,
@@ -109,22 +202,77 @@ ipcMain.handle(
SettingEmsOutFilePathSet,
);
+// Paint Scale Input Settings Handlers
+ipcMain.handle(
+ ipcTypes.toMain.settings.paintScale.getInputConfigs,
+ SettingsPaintScaleInputConfigsGet,
+);
+ipcMain.handle(
+ ipcTypes.toMain.settings.paintScale.setInputConfigs,
+ SettingsPaintScaleInputConfigsSet,
+);
+ipcMain.handle(
+ ipcTypes.toMain.settings.paintScale.setInputPath,
+ SettingsPaintScaleInputPathSet,
+);
+
+// Paint Scale Output Settings Handlers
+ipcMain.handle(
+ ipcTypes.toMain.settings.paintScale.getOutputConfigs,
+ SettingsPaintScaleOutputConfigsGet,
+);
+ipcMain.handle(
+ ipcTypes.toMain.settings.paintScale.setOutputConfigs,
+ SettingsPaintScaleOutputConfigsSet,
+);
+ipcMain.handle(
+ ipcTypes.toMain.settings.paintScale.setOutputPath,
+ SettingsPaintScaleOutputPathSet,
+);
+
+// IPC handlers for updating paint scale cron
+ipcMain.on(
+ ipcTypes.toMain.settings.paintScale.updateInputCron,
+ (_event, configs: PaintScaleConfig[]) => {
+ handlePaintScaleInputCron(configs).catch((error) => {
+ log.error(`Error handling paint scale input cron for configs: ${error}`);
+ });
+ },
+);
+
+ipcMain.on(
+ ipcTypes.toMain.settings.paintScale.updateOutputCron,
+ (_event, configs: PaintScaleConfig[]) => {
+ handlePaintScaleOutputCron(configs).catch((error) => {
+ log.error(`Error handling paint scale output cron for configs: ${error}`);
+ });
+ },
+);
+
ipcMain.handle(ipcTypes.toMain.user.getActiveShop, () => {
return store.get("app.bodyshop.shopname");
});
-//Watcher Handlers
+// Watcher Handlers
ipcMain.on(ipcTypes.toMain.watcher.start, () => {
- StartWatcher();
+ StartWatcher().catch((error) => {
+ log.error("Error starting watcher:", error);
+ });
});
ipcMain.on(ipcTypes.toMain.watcher.stop, () => {
- StopWatcher();
+ StopWatcher().catch((error) => {
+ log.error("Error stopping watcher:", error);
+ });
});
ipcMain.on(ipcTypes.toMain.updates.download, () => {
log.info("Download update requested from renderer.");
- autoUpdater.downloadUpdate();
+ autoUpdater.downloadUpdate().catch((error) => {
+ log.error("Error downloading update:", error);
+ });
});
+export { initializeCronTasks };
+
logIpcMessages();
diff --git a/src/main/ipc/ipcMainConfig.types.ts b/src/main/ipc/ipcMainConfig.types.ts
new file mode 100644
index 0000000..c90c9b4
--- /dev/null
+++ b/src/main/ipc/ipcMainConfig.types.ts
@@ -0,0 +1,67 @@
+export interface User {
+ stsTokenManager?: {
+ accessToken: string;
+ };
+}
+
+export interface BodyShop {
+ shopname: string;
+ id: string;
+}
+
+export interface GraphQLResponse {
+ bodyshops_by_pk?: {
+ imexshopid: string;
+ shopname: string;
+ };
+ jobs?: Array<{
+ labhrs: any;
+ larhrs: any;
+ ro_number: string;
+ ownr_ln: string;
+ ownr_fn: string;
+ plate_no: string;
+ v_vin: string;
+ v_model_yr: string;
+ v_make_desc: string;
+ v_model_desc: string;
+ vehicle?: {
+ v_paint_codes?: {
+ paint_cd1: string;
+ };
+ };
+ larhrs_aggregate?: {
+ aggregate?: {
+ sum?: {
+ mod_lb_hrs: number;
+ };
+ };
+ };
+ ins_co_nm: string;
+ est_ct_ln: string;
+ est_ct_fn: string;
+ job_totals?: {
+ rates?: {
+ mapa?: {
+ total?: {
+ amount: number;
+ };
+ };
+ };
+ totals?: {
+ subtotal?: {
+ amount: number;
+ };
+ };
+ };
+ rate_mapa: number;
+ labhrs_aggregate?: {
+ aggregate?: {
+ sum?: {
+ mod_lb_hrs: number;
+ };
+ };
+ };
+ rate_lab: number;
+ }>;
+}
\ No newline at end of file
diff --git a/src/main/ipc/ipcMainHandler.settings.ts b/src/main/ipc/ipcMainHandler.settings.ts
index a066d79..b71b636 100644
--- a/src/main/ipc/ipcMainHandler.settings.ts
+++ b/src/main/ipc/ipcMainHandler.settings.ts
@@ -1,9 +1,27 @@
-import {dialog, IpcMainInvokeEvent} from "electron";
+// main/ipcMainHandler.settings.ts
+import { dialog, IpcMainInvokeEvent } from "electron";
import log from "electron-log/main";
import _ from "lodash";
import Store from "../store/store";
-import {getMainWindow} from "../util/toRenderer";
-import {addWatcherPath, removeWatcherPath, StartWatcher, StopWatcher,} from "../watcher/watcher";
+import { getMainWindow } from "../util/toRenderer";
+import {
+ addWatcherPath,
+ removeWatcherPath,
+ StartWatcher,
+ StopWatcher,
+} from "../watcher/watcher";
+import { PaintScaleConfig } from "../../util/types/paintScale";
+
+
+// Initialize paint scale input configs in store if not set
+if (!Store.get("settings.paintScaleInputConfigs")) {
+ Store.set("settings.paintScaleInputConfigs", []);
+}
+
+// Initialize paint scale output configs in store if not set
+if (!Store.get("settings.paintScaleOutputConfigs")) {
+ Store.set("settings.paintScaleOutputConfigs", []);
+}
const SettingsWatchedFilePathsAdd = async (): Promise => {
const mainWindow = getMainWindow();
@@ -17,21 +35,22 @@ const SettingsWatchedFilePathsAdd = async (): Promise => {
if (!result.canceled) {
Store.set(
- "settings.filepaths",
- _.union(result.filePaths, Store.get("settings.filepaths")),
+ "settings.filepaths",
+ _.union(result.filePaths, Store.get("settings.filepaths")),
);
addWatcherPath(result.filePaths);
}
return Store.get("settings.filepaths");
};
+
const SettingsWatchedFilePathsRemove = async (
- _event: IpcMainInvokeEvent,
- path: string,
+ _event: IpcMainInvokeEvent,
+ path: string,
): Promise => {
Store.set(
- "settings.filepaths",
- _.without(Store.get("settings.filepaths"), path),
+ "settings.filepaths",
+ _.without(Store.get("settings.filepaths"), path),
);
removeWatcherPath(path);
return Store.get("settings.filepaths");
@@ -46,15 +65,16 @@ const SettingsWatcherPollingGet = async (): Promise<{
interval: number;
}> => {
const pollingEnabled: { enabled: boolean; interval: number } =
- Store.get("settings.polling");
+ Store.get("settings.polling");
return { enabled: pollingEnabled.enabled, interval: pollingEnabled.interval };
};
+
const SettingsWatcherPollingSet = async (
- _event: IpcMainInvokeEvent,
- pollingSettings: {
- enabled: boolean;
- interval: number;
- },
+ _event: IpcMainInvokeEvent,
+ pollingSettings: {
+ enabled: boolean;
+ interval: number;
+ },
): Promise<{
enabled: boolean;
interval: number;
@@ -63,15 +83,16 @@ const SettingsWatcherPollingSet = async (
const { enabled, interval } = pollingSettings;
Store.set("settings.polling", { enabled, interval });
- //Restart the watcher with these new settings.
await StopWatcher();
await StartWatcher();
return { enabled, interval };
};
+
const SettingsPpcFilePathGet = async (): Promise => {
return Store.get("settings.ppcFilePath");
};
+
const SettingsPpcFilePathSet = async (): Promise => {
const mainWindow = getMainWindow();
if (!mainWindow) {
@@ -83,14 +104,16 @@ const SettingsPpcFilePathSet = async (): Promise => {
});
if (!result.canceled) {
- Store.set("settings.ppcFilePath", result.filePaths[0]); //There should only ever be on directory that was selected.
+ Store.set("settings.ppcFilePath", result.filePaths[0]);
}
return (Store.get("settings.ppcFilePath") as string) || "";
};
+
const SettingEmsOutFilePathGet = async (): Promise => {
return Store.get("settings.emsOutFilePath");
};
+
const SettingEmsOutFilePathSet = async (): Promise => {
const mainWindow = getMainWindow();
if (!mainWindow) {
@@ -102,12 +125,116 @@ const SettingEmsOutFilePathSet = async (): Promise => {
});
if (!result.canceled) {
- Store.set("settings.emsOutFilePath", result.filePaths[0]); //There should only ever be on directory that was selected.
+ Store.set("settings.emsOutFilePath", result.filePaths[0]);
}
return (Store.get("settings.emsOutFilePath") as string) || "";
};
+const SettingsPaintScaleInputConfigsGet = async (
+ _event?: IpcMainInvokeEvent,
+): Promise => {
+ try {
+ const configs = Store.get("settings.paintScaleInputConfigs") as PaintScaleConfig[];
+ log.debug("Retrieved paint scale input configs:", configs);
+ return configs || [];
+ } catch (error) {
+ log.error("Error getting paint scale input configs:", error);
+ throw error;
+ }
+};
+
+const SettingsPaintScaleInputConfigsSet = async (
+ _event: IpcMainInvokeEvent,
+ configs: PaintScaleConfig[],
+): Promise => {
+ try {
+ Store.set("settings.paintScaleInputConfigs", configs);
+ log.debug("Saved paint scale input configs:", configs);
+ return true;
+ } catch (error) {
+ log.error("Error setting paint scale input configs:", error);
+ throw error;
+ }
+};
+
+const SettingsPaintScaleInputPathSet = async (
+ _event: IpcMainInvokeEvent,
+): Promise => {
+ try {
+ const mainWindow = getMainWindow();
+ if (!mainWindow) {
+ log.error("No main window found when trying to open dialog");
+ return null;
+ }
+ const result = await dialog.showOpenDialog(mainWindow, {
+ properties: ["openDirectory"],
+ });
+ if (result.canceled) {
+ log.debug("Paint scale input path selection canceled");
+ return null;
+ }
+ const path = result.filePaths[0];
+ log.debug("Selected paint scale input path:", path);
+ return path;
+ } catch (error) {
+ log.error("Error setting paint scale input path:", error);
+ throw error;
+ }
+};
+
+const SettingsPaintScaleOutputConfigsGet = async (
+ _event?: IpcMainInvokeEvent,
+): Promise => {
+ try {
+ const configs = Store.get("settings.paintScaleOutputConfigs") as PaintScaleConfig[];
+ log.debug("Retrieved paint scale output configs:", configs);
+ return configs || [];
+ } catch (error) {
+ log.error("Error getting paint scale output configs:", error);
+ throw error;
+ }
+};
+
+const SettingsPaintScaleOutputConfigsSet = async (
+ _event: IpcMainInvokeEvent,
+ configs: PaintScaleConfig[],
+): Promise => {
+ try {
+ Store.set("settings.paintScaleOutputConfigs", configs);
+ log.debug("Saved paint scale output configs:", configs);
+ return true;
+ } catch (error) {
+ log.error("Error setting paint scale output configs:", error);
+ throw error;
+ }
+};
+
+const SettingsPaintScaleOutputPathSet = async (
+ _event: IpcMainInvokeEvent,
+): Promise => {
+ try {
+ const mainWindow = getMainWindow();
+ if (!mainWindow) {
+ log.error("No main window found when trying to open dialog");
+ return null;
+ }
+ const result = await dialog.showOpenDialog(mainWindow, {
+ properties: ["openDirectory"],
+ });
+ if (result.canceled) {
+ log.debug("Paint scale output path selection canceled");
+ return null;
+ }
+ const path = result.filePaths[0];
+ log.debug("Selected paint scale output path:", path);
+ return path;
+ } catch (error) {
+ log.error("Error setting paint scale output path:", error);
+ throw error;
+ }
+};
+
export {
SettingsPpcFilePathGet,
SettingsPpcFilePathSet,
@@ -118,4 +245,10 @@ export {
SettingsWatcherPollingSet,
SettingEmsOutFilePathGet,
SettingEmsOutFilePathSet,
-};
+ SettingsPaintScaleInputConfigsGet,
+ SettingsPaintScaleInputConfigsSet,
+ SettingsPaintScaleInputPathSet,
+ SettingsPaintScaleOutputConfigsGet,
+ SettingsPaintScaleOutputConfigsSet,
+ SettingsPaintScaleOutputPathSet,
+};
\ No newline at end of file
diff --git a/src/main/ipc/paintScaleHandlers/PPG.ts b/src/main/ipc/paintScaleHandlers/PPG.ts
new file mode 100644
index 0000000..da66331
--- /dev/null
+++ b/src/main/ipc/paintScaleHandlers/PPG.ts
@@ -0,0 +1,261 @@
+import log from "electron-log/main";
+import path from "path";
+import fs from "fs/promises";
+import axios from "axios";
+import { create } from "xmlbuilder2";
+import { parseStringPromise } from "xml2js";
+import store from "../../store/store";
+import client from "../../graphql/graphql-client";
+import { PaintScaleConfig } from "../../../util/types/paintScale";
+
+// PPG Input Handler
+export async function ppgInputHandler(config: PaintScaleConfig): Promise {
+ try {
+ log.info(
+ `Polling input directory for PPG config ${config.id}: ${config.path}`,
+ );
+
+ // Ensure archive and error directories exist
+ const archiveDir = path.join(config.path!, "archive");
+ const errorDir = path.join(config.path!, "error");
+ await fs.mkdir(archiveDir, { recursive: true });
+ await fs.mkdir(errorDir, { recursive: true });
+
+ // Check for files
+ const files = await fs.readdir(config.path!);
+ for (const file of files) {
+ // Only process XML files
+ if (!file.toLowerCase().endsWith(".xml")) {
+ continue;
+ }
+
+ const filePath = path.join(config.path!, file);
+ const stats = await fs.stat(filePath);
+ if (!stats.isFile()) {
+ continue;
+ }
+
+ log.debug(`Processing input file: ${filePath}`);
+
+ // Check file accessibility (e.g., not locked)
+ try {
+ await fs.access(filePath, fs.constants.R_OK);
+ } catch (error) {
+ log.warn(`File ${filePath} is inaccessible, skipping:`, error);
+ continue;
+ }
+
+ // Validate XML structure
+ let xmlContent : BlobPart;
+
+ try {
+ xmlContent = await fs.readFile(filePath, "utf8");
+ await parseStringPromise(xmlContent);
+ } catch (error) {
+ log.error(`Invalid XML in ${filePath}:`, error);
+ const timestamp = Date.now().toString(); // similar to DateTime.Now.Ticks in C#
+ const errorPath = path.join(errorDir, `${timestamp}.xml`);
+ await fs.rename(filePath, errorPath);
+ log.debug(`Moved invalid file to error: ${errorPath}`);
+ continue;
+ }
+
+ // Get authentication token
+ const token = (store.get("user") as any)?.stsTokenManager?.accessToken;
+ if (!token) {
+ log.error(`No authentication token for file: ${filePath}`);
+ continue;
+ }
+
+ // Upload file to API
+ const formData = new FormData();
+ formData.append("file", new Blob([xmlContent]), path.basename(filePath));
+ formData.append(
+ "shopId",
+ (store.get("app.bodyshop") as any)?.shopname || "",
+ );
+
+ const baseURL = store.get("app.isTest")
+ ? import.meta.env.VITE_API_TEST_URL
+ : import.meta.env.VITE_API_URL;
+ const finalUrl = `${baseURL}/mixdata/upload`;
+
+ log.debug(`Uploading file to ${finalUrl}`);
+
+ try {
+ const response = await axios.post(finalUrl, formData, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "multipart/form-data",
+ },
+ });
+
+ if (response.status === 200) {
+ log.info(`Successful upload of ${filePath}`);
+ // Move file to archive
+ const timestamp = Date.now().toString(); // generate new timestamp
+ const archivePath = path.join(archiveDir, `${timestamp}.xml`);
+ await fs.rename(filePath, archivePath);
+ log.debug(`Moved file to archive: ${archivePath}`);
+ } else {
+ log.error(
+ `Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
+ response.data,
+ );
+ }
+ } catch (error) {
+ log.error(`Error uploading ${filePath}:`, error);
+ }
+ }
+ } catch (error) {
+ log.error(`Error polling input directory ${config.path}:`, error);
+ }
+}
+
+// PPG Output Handler
+export async function ppgOutputHandler(
+ config: PaintScaleConfig,
+): Promise {
+ try {
+ log.info(`Generating PPG output for config ${config.id}: ${config.path}`);
+
+ await fs.mkdir(config.path!, { recursive: true });
+
+ const query = `
+ query PpgData($today: timestamptz!, $todayplus5: timestamptz!, $shopid: uuid!) {
+ bodyshops_by_pk(id:$shopid) {
+ id
+ shopname
+ imexshopid
+ }
+ jobs(where: {
+ _or: [
+ {
+ _and: [
+ { scheduled_in: { _lte: $todayplus5 } },
+ { scheduled_in: { _gte: $today } }
+ ]
+ },
+ { inproduction: { _eq: true } }
+ ]
+ }) {
+ id
+ ro_number
+ status
+ ownr_fn
+ ownr_ln
+ ownr_co_nm
+ v_vin
+ v_model_yr
+ v_make_desc
+ v_model_desc
+ v_color
+ plate_no
+ ins_co_nm
+ est_ct_fn
+ est_ct_ln
+ rate_mapa
+ rate_lab
+ job_totals
+ vehicle {
+ v_paint_codes
+ }
+ labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
+ aggregate {
+ sum {
+ mod_lb_hrs
+ }
+ }
+ }
+ larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
+ aggregate {
+ sum {
+ mod_lb_hrs
+ }
+ }
+ }
+ }
+ }
+`;
+
+ const variables = {
+ today: new Date().toISOString(),
+ todayplus5: new Date(Date.now() + 5 * 86400000).toISOString(),
+ shopid: (store.get("app.bodyshop") as any)?.id,
+ };
+
+ const response = (await client.request(query, variables)) as any;
+ const jobs = response.jobs ?? [];
+
+ const header = {
+ PPG: {
+ Header: {
+ Protocol: {
+ Message: "PaintShopInterface",
+ Name: "PPG",
+ Version: "1.5.0",
+ },
+ Transaction: {
+ TransactionID: "",
+ TransactionDate: (() => {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = ("0" + (now.getMonth() + 1)).slice(-2);
+ const day = ("0" + now.getDate()).slice(-2);
+ const hours = ("0" + now.getHours()).slice(-2);
+ const minutes = ("0" + now.getMinutes()).slice(-2);
+ return `${year}-${month}-${day}:${hours}:${minutes}`;
+ })(),
+ },
+ Product: {
+ Name: "ImEX Online",
+ Version: "",
+ },
+ },
+ DataInterface: {
+ ROData: {
+ ShopInfo: {
+ ShopID: response.bodyshops_by_pk?.imexshopid || "",
+ ShopName: response.bodyshops_by_pk?.shopname || "",
+ },
+ RepairOrders: {
+ ROCount: jobs.length.toString(),
+ RO: jobs.map((job: any) => ({
+ RONumber: job.ro_number || "",
+ ROStatus: "Open",
+ Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`,
+ ROPainterNotes: "",
+ LicensePlateNum: job.plate_no || "",
+ VIN: job.v_vin || "",
+ ModelYear: job.v_model_yr || "",
+ MakeDesc: job.v_make_desc || "",
+ ModelName: job.v_model_desc || "",
+ OEMColorCode: job.vehicle?.v_paint_codes?.paint_cd1 || "",
+ RefinishLaborHours: job.larhrs?.aggregate?.sum?.mod_lb_hrs || 0,
+ InsuranceCompanyName: job.ins_co_nm || "",
+ EstimatorName: `${job.est_ct_ln || ""}, ${job.est_ct_fn || ""}`,
+ PaintMaterialsRevenue: (
+ (job.job_totals?.rates?.mapa?.total?.amount || 0) / 100
+ ).toFixed(2),
+ PaintMaterialsRate: job.rate_mapa || 0,
+ BodyHours: job.labhrs?.aggregate?.sum?.mod_lb_hrs || 0,
+ BodyLaborRate: job.rate_lab || 0,
+ TotalCostOfRepairs: (
+ (job.job_totals?.totals?.subtotal?.amount || 0) / 100
+ ).toFixed(2),
+ })),
+ },
+ },
+ },
+ },
+ };
+
+ const xml = create({ version: "1.0" }, header).end({ prettyPrint: true });
+ const outputPath = path.join(config.path!, `PPGPaint.xml`);
+ await fs.writeFile(outputPath, xml);
+ log.info(`Saved PPG output XML to ${outputPath}`);
+ } catch (error) {
+ log.error(`Error generating PPG output for config ${config.id}:`, error);
+ }
+}
+
diff --git a/src/main/setup-keep-alive-agent.ts b/src/main/setup-keep-alive-agent.ts
new file mode 100644
index 0000000..60ae2b4
--- /dev/null
+++ b/src/main/setup-keep-alive-agent.ts
@@ -0,0 +1,71 @@
+import { promises as fs } from "fs";
+import { join } from "path";
+import { homedir } from "os";
+import { exec } from "child_process";
+import { promisify } from "util";
+import log from "electron-log/main";
+
+const execPromise = promisify(exec);
+
+// Define the interval as a variable (in seconds)
+const KEEP_ALIVE_INTERVAL_SECONDS = 15 * 60; // 15 minutes
+
+export async function setupKeepAliveAgent(): Promise {
+ const plistContent = `
+
+
+
+ Label
+ com.convenientbrands.bodyshop-desktop.keepalive
+ ProgramArguments
+
+ open
+ imexmedia://keep-alive
+
+ RunAtLoad
+
+ StartInterval
+ ${KEEP_ALIVE_INTERVAL_SECONDS}
+
+`;
+
+ const plistPath = join(
+ homedir(),
+ "Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
+ );
+
+ try {
+ await fs.writeFile(plistPath, plistContent);
+ const { stdout, stderr } = await execPromise(`launchctl load ${plistPath}`);
+ log.info(`Launch agent created and loaded: ${stdout}`);
+ if (stderr) log.warn(`Launch agent stderr: ${stderr}`);
+ } catch (error) {
+ log.error(`Error setting up launch agent: ${error instanceof Error ? error.message : String(error)}`);
+ throw error; // Rethrow to allow caller to handle
+ }
+}
+
+export async function isKeepAliveAgentInstalled(): Promise {
+ const plistPath = join(
+ homedir(),
+ "Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
+ );
+ const maxRetries = 3;
+ const retryDelay = 500; // 500ms delay between retries
+
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ await fs.access(plistPath, fs.constants.F_OK);
+ const { stdout } = await execPromise(`launchctl list | grep com.convenientbrands.bodyshop-desktop.keepalive`);
+ return !!stdout; // Return true if plist exists and agent is loaded
+ } catch (error) {
+ log.debug(`Launch agent not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`);
+ if (attempt === maxRetries) {
+ return false; // Return false after all retries fail
+ }
+ // Wait before retrying
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
+ }
+ }
+ return false; // Fallback return
+}
\ No newline at end of file
diff --git a/src/main/setup-keep-alive-task.ts b/src/main/setup-keep-alive-task.ts
new file mode 100644
index 0000000..a0af962
--- /dev/null
+++ b/src/main/setup-keep-alive-task.ts
@@ -0,0 +1,53 @@
+import { exec } from "child_process";
+import { promisify } from "util";
+import log from "electron-log/main";
+
+const execPromise = promisify(exec);
+
+// Define the interval as a variable (in minutes)
+const KEEP_ALIVE_INTERVAL_MINUTES = 15;
+
+export async function setupKeepAliveTask(): Promise {
+ const taskName = "ImEXShopPartnerKeepAlive";
+ const protocolUrl = "imexmedia://keep-alive";
+ // Use rundll32.exe to silently open the URL as a protocol
+ const command = `rundll32.exe url.dll,OpenURL "${protocolUrl}"`;
+ // Escape quotes for schtasks /tr parameter
+ const escapedCommand = command.replace(/"/g, '\\"');
+
+ const schtasksCommand = `schtasks /create /tn "${taskName}" /tr "${escapedCommand}" /sc minute /mo ${KEEP_ALIVE_INTERVAL_MINUTES} /f`;
+
+ try {
+ const { stdout, stderr } = await execPromise(schtasksCommand);
+ log.info(`Scheduled task created: ${stdout}`);
+ if (stderr) log.warn(`Scheduled task stderr: ${stderr}`);
+ } catch (error) {
+ log.error(
+ `Error creating scheduled task: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ throw error; // Rethrow to allow caller to handle
+ }
+}
+
+export async function isKeepAliveTaskInstalled(): Promise {
+ const taskName = "ImEXShopPartnerKeepAlive";
+ const maxRetries = 3;
+ const retryDelay = 500; // 500ms delay between retries
+
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ const { stdout } = await execPromise(`schtasks /query /tn "${taskName}"`);
+ return !!stdout; // Return true if task exists
+ } catch (error) {
+ log.debug(
+ `Scheduled task ${taskName} not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`,
+ );
+ if (attempt === maxRetries) {
+ return false; // Return false after all retries fail
+ }
+ // Wait before retrying
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
+ }
+ }
+ return false; // Fallback return
+}
diff --git a/src/renderer/src/components/Settings/PaintScale/usePaintScaleConfig.ts b/src/renderer/src/components/Settings/PaintScale/usePaintScaleConfig.ts
new file mode 100644
index 0000000..f87be8f
--- /dev/null
+++ b/src/renderer/src/components/Settings/PaintScale/usePaintScaleConfig.ts
@@ -0,0 +1,131 @@
+import { useState, useEffect } from 'react';
+import ipcTypes from '../../../../../util/ipcTypes.json';
+import { PaintScaleConfig, PaintScaleType } from '../../../../../util/types/paintScale';
+import { message } from "antd";
+import {useTranslation} from "react-i18next";
+
+type ConfigType = 'input' | 'output';
+
+export const usePaintScaleConfig = (configType: ConfigType) => {
+ const [paintScaleConfigs, setPaintScaleConfigs] = useState([]);
+ const { t } = useTranslation();
+
+ // Get the appropriate IPC methods based on config type
+ const getConfigsMethod = configType === 'input'
+ ? ipcTypes.toMain.settings.paintScale.getInputConfigs
+ : ipcTypes.toMain.settings.paintScale.getOutputConfigs;
+
+ const setConfigsMethod = configType === 'input'
+ ? ipcTypes.toMain.settings.paintScale.setInputConfigs
+ : ipcTypes.toMain.settings.paintScale.setOutputConfigs;
+
+ const setPathMethod = configType === 'input'
+ ? ipcTypes.toMain.settings.paintScale.setInputPath
+ : ipcTypes.toMain.settings.paintScale.setOutputPath;
+
+ // Load paint scale configs on mount
+ useEffect(() => {
+ window.electron.ipcRenderer
+ .invoke(getConfigsMethod)
+ .then((configs: PaintScaleConfig[]) => {
+ // Ensure all configs have a pollingInterval and type (for backward compatibility)
+ const updatedConfigs = configs.map(config => ({
+ ...config,
+ pollingInterval: config.pollingInterval || 1440, // Default to 1440 seconds
+ type: config.type || PaintScaleType.PPG, // Default type if missing
+ }));
+ setPaintScaleConfigs(updatedConfigs || []);
+ })
+ .catch((error) => {
+ console.error(`Failed to load paint scale ${configType} configs:`, error);
+ });
+ }, [getConfigsMethod]);
+
+ // Save configs to store and notify main process of config changes
+ const saveConfigs = (configs: PaintScaleConfig[]) => {
+ window.electron.ipcRenderer
+ .invoke(setConfigsMethod, configs)
+ .then(() => {
+ // Notify main process to update cron job
+ if (configType === 'input') {
+ window.electron.ipcRenderer.send(ipcTypes.toMain.settings.paintScale.updateInputCron, configs);
+ } else if (configType === 'output') {
+ window.electron.ipcRenderer.send(ipcTypes.toMain.settings.paintScale.updateOutputCron, configs);
+ }
+ })
+ .catch((error) => {
+ console.error(`Failed to save paint scale ${configType} configs:`, error);
+ });
+ };
+
+ // New helper to check if a path is unique across input and output configs
+ const checkPathUnique = async (newPath: string): Promise => {
+ try {
+ const inputConfigs: PaintScaleConfig[] = await window.electron.ipcRenderer.invoke(ipcTypes.toMain.settings.paintScale.getInputConfigs);
+ const outputConfigs: PaintScaleConfig[] = await window.electron.ipcRenderer.invoke(ipcTypes.toMain.settings.paintScale.getOutputConfigs);
+ const allConfigs = [...inputConfigs, ...outputConfigs];
+ // Allow updating the current config even if its current value equals newPath.
+ return !allConfigs.some(config => config.path === newPath);
+ } catch (error) {
+ console.error("Failed to check unique path:", error);
+ return false;
+ }
+ };
+
+ // Handle adding a new paint scale config
+ const handleAddConfig = (type: PaintScaleType) => {
+ const newConfig: PaintScaleConfig = {
+ id: Date.now().toString(),
+ type,
+ pollingInterval: 1440, // Default to 1440 seconds
+ };
+ const updatedConfigs = [...paintScaleConfigs, newConfig];
+ setPaintScaleConfigs(updatedConfigs);
+ saveConfigs(updatedConfigs);
+ };
+
+ // Handle removing a config
+ const handleRemoveConfig = (id: string) => {
+ const updatedConfigs = paintScaleConfigs.filter((config) => config.id !== id);
+ setPaintScaleConfigs(updatedConfigs);
+ saveConfigs(updatedConfigs);
+ };
+
+ // Handle path selection (modified to check directory uniqueness)
+ const handlePathChange = async (id: string) => {
+ try {
+ const path: string | null = await window.electron.ipcRenderer.invoke(setPathMethod, id);
+ if (path) {
+ const isUnique = await checkPathUnique(path);
+ if (!isUnique) {
+ message.error(t("settings.errors.duplicatePath"));
+ return;
+ }
+ const updatedConfigs = paintScaleConfigs.map((config) =>
+ config.id === id ? { ...config, path } : config,
+ );
+ setPaintScaleConfigs(updatedConfigs);
+ saveConfigs(updatedConfigs);
+ }
+ } catch (error) {
+ console.error(`Failed to set paint scale ${configType} path:`, error);
+ }
+ };
+
+ // Handle polling interval change
+ const handlePollingIntervalChange = (id: string, pollingInterval: number) => {
+ const updatedConfigs = paintScaleConfigs.map((config) =>
+ config.id === id ? { ...config, pollingInterval } : config,
+ );
+ setPaintScaleConfigs(updatedConfigs);
+ saveConfigs(updatedConfigs);
+ };
+
+ return {
+ paintScaleConfigs,
+ handleAddConfig,
+ handleRemoveConfig,
+ handlePathChange,
+ handlePollingIntervalChange,
+ };
+};
diff --git a/src/renderer/src/components/Settings/Settings.PaintScaleInputPaths.tsx b/src/renderer/src/components/Settings/Settings.PaintScaleInputPaths.tsx
new file mode 100644
index 0000000..444047b
--- /dev/null
+++ b/src/renderer/src/components/Settings/Settings.PaintScaleInputPaths.tsx
@@ -0,0 +1,180 @@
+import {
+ CheckCircleFilled,
+ FileAddFilled,
+ FolderOpenFilled,
+ WarningFilled,
+} from "@ant-design/icons";
+import {
+ Button,
+ Card,
+ Input,
+ Modal,
+ Select,
+ Space,
+ Table,
+ Tag,
+ Tooltip,
+} from "antd";
+import { FC, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ PaintScaleConfig,
+ PaintScaleType,
+ paintScaleTypeOptions,
+} from "../../../../util/types/paintScale";
+import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
+
+const SettingsPaintScaleInputPaths: FC = () => {
+ const { t } = useTranslation();
+ const {
+ paintScaleConfigs,
+ handleAddConfig,
+ handleRemoveConfig,
+ handlePathChange,
+ handlePollingIntervalChange,
+ } = usePaintScaleConfig("input");
+
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [selectedType, setSelectedType] = useState(null);
+
+ // Show modal when adding a new path
+ const showAddPathModal = () => {
+ setSelectedType(null);
+ setIsModalVisible(true);
+ };
+
+ // Handle modal confirmation
+ const handleModalOk = () => {
+ if (selectedType) {
+ handleAddConfig(selectedType);
+ setIsModalVisible(false);
+ }
+ };
+
+ // Handle modal cancellation
+ const handleModalCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ // Table columns for paint scale configs
+ const columns = [
+ {
+ title: t("settings.labels.paintScaleType"),
+ dataIndex: "type",
+ key: "type",
+ render: (type: PaintScaleType) => {
+ const typeOption = paintScaleTypeOptions.find(
+ (option) => option.value === type,
+ );
+ const label = typeOption ? typeOption.label : type;
+ const colorMap: Partial> = {
+ [PaintScaleType.PPG]: "blue",
+ // Add other types and colors as needed
+ };
+ return {label};
+ },
+ },
+ {
+ title: t("settings.labels.paintScalePath"),
+ dataIndex: "path",
+ key: "path",
+ render: (path: string | null, record: PaintScaleConfig) => {
+ const isValid = path && path.trim() !== "";
+ return (
+
+
+ {isValid ? (
+
+ ) : (
+
+ )}
+
+ }
+ />
+
+ );
+ },
+ },
+ {
+ title: t("settings.labels.pollingInterval"),
+ dataIndex: "pollingInterval",
+ key: "pollingInterval",
+ render: (pollingInterval: number, record: PaintScaleConfig) => (
+
+ handlePollingIntervalChange(record.id, Number(e.target.value))
+ }
+ style={{ width: 100 }}
+ placeholder={t("settings.labels.pollingInterval")}
+ />
+ ),
+ },
+ {
+ title: t("settings.labels.actions"),
+ key: "actions",
+ render: (_: any, record: PaintScaleConfig) => (
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+ }>
+ {t("settings.actions.addpath")}
+
+ }
+ >
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsPaintScaleInputPaths;
diff --git a/src/renderer/src/components/Settings/Settings.PaintScaleOutputPaths.tsx b/src/renderer/src/components/Settings/Settings.PaintScaleOutputPaths.tsx
new file mode 100644
index 0000000..d3f695d
--- /dev/null
+++ b/src/renderer/src/components/Settings/Settings.PaintScaleOutputPaths.tsx
@@ -0,0 +1,162 @@
+import {
+ CheckCircleFilled,
+ FileAddFilled,
+ FolderOpenFilled,
+ WarningFilled,
+} from "@ant-design/icons";
+import { Button, Card, Input, Modal, Select, Space, Table, Tag } from "antd";
+import { FC, useState } from "react";
+import { useTranslation } from "react-i18next";
+import {
+ PaintScaleConfig,
+ PaintScaleType,
+ paintScaleTypeOptions,
+} from "../../../../util/types/paintScale";
+import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
+
+const SettingsPaintScaleOutputPaths: FC = () => {
+ const { t } = useTranslation();
+ const {
+ paintScaleConfigs,
+ handleAddConfig,
+ handleRemoveConfig,
+ handlePathChange,
+ handlePollingIntervalChange,
+ } = usePaintScaleConfig("output");
+
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const [selectedType, setSelectedType] = useState(null);
+
+ // Show modal when adding a new path
+ const showAddPathModal = () => {
+ setSelectedType(null);
+ setIsModalVisible(true);
+ };
+
+ // Handle modal confirmation
+ const handleModalOk = () => {
+ if (selectedType) {
+ handleAddConfig(selectedType);
+ setIsModalVisible(false);
+ }
+ };
+
+ // Handle modal cancellation
+ const handleModalCancel = () => {
+ setIsModalVisible(false);
+ };
+
+ // Table columns for paint scale configs
+ const columns = [
+ {
+ title: t("settings.labels.paintScaleType"),
+ dataIndex: "type",
+ key: "type",
+ render: (type: PaintScaleType) => {
+ const typeOption = paintScaleTypeOptions.find(
+ (option) => option.value === type,
+ );
+ const label = typeOption ? typeOption.label : type;
+ const colorMap: Partial> = {
+ [PaintScaleType.PPG]: "blue",
+ // Add other types and colors as needed
+ };
+ return {label};
+ },
+ },
+ {
+ title: t("settings.labels.paintScalePath"),
+ dataIndex: "path",
+ key: "path",
+ render: (path: string | null, record: PaintScaleConfig) => {
+ const isValid = path && path.trim() !== "";
+ return (
+
+
+ ) : (
+
+ )
+ }
+ />
+
+ );
+ },
+ },
+ {
+ title: t("settings.labels.pollingInterval"),
+ dataIndex: "pollingInterval",
+ key: "pollingInterval",
+ render: (pollingInterval: number, record: PaintScaleConfig) => (
+
+ handlePollingIntervalChange(record.id, Number(e.target.value))
+ }
+ style={{ width: 100 }}
+ placeholder={t("settings.labels.pollingInterval")}
+ />
+ ),
+ },
+ {
+ title: t("settings.labels.actions"),
+ key: "actions",
+ render: (_: any, record: PaintScaleConfig) => (
+
+ ),
+ },
+ ];
+
+ return (
+ <>
+ }>
+ {t("settings.actions.addpath")}
+
+ }
+ >
+
+
+
+
+
+ >
+ );
+};
+
+export default SettingsPaintScaleOutputPaths;
diff --git a/src/renderer/src/components/Settings/Settings.tsx b/src/renderer/src/components/Settings/Settings.tsx
index 0e71702..5cdb13b 100644
--- a/src/renderer/src/components/Settings/Settings.tsx
+++ b/src/renderer/src/components/Settings/Settings.tsx
@@ -1,3 +1,4 @@
+// renderer/Settings.tsx
import { Col, Row } from "antd";
import { FC } from "react";
import SettingsWatchedPaths from "./Settings.WatchedPaths";
@@ -5,30 +6,40 @@ import SettingsWatcher from "./Settings.Watcher";
import Welcome from "../Welcome/Welcome";
import SettingsPpcFilepath from "./Settings.PpcFilePath";
import SettingsEmsOutFilePath from "./Settings.EmsOutFilePath";
+import SettingsPaintScaleInputPaths from "./Settings.PaintScaleInputPaths";
+import SettingsPaintScaleOutputPaths from "./Settings.PaintScaleOutputPaths";
const colSpans = {
- md: 12,
- sm: 24,
+ md: 12, // Two columns on medium screens and above
+ sm: 24, // One column on small screens
};
+
const Settings: FC = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
};
-export default Settings;
+
+export default Settings;
\ No newline at end of file
diff --git a/src/renderer/src/util/graphql.client.ts b/src/renderer/src/util/graphql.client.ts
index 5ddac51..939baa0 100644
--- a/src/renderer/src/util/graphql.client.ts
+++ b/src/renderer/src/util/graphql.client.ts
@@ -1,13 +1,13 @@
import {
ApolloClient,
ApolloLink,
- HttpLink,
+ // HttpLink,
InMemoryCache,
} from "@apollo/client";
-const httpLink: HttpLink = new HttpLink({
- uri: import.meta.env.VITE_GRAPHQL_URL,
-});
+// const httpLink: HttpLink = new HttpLink({
+// uri: import.meta.env.VITE_GRAPHQL_URL,
+// });
const middlewares = [];
diff --git a/src/util/ipcTypes.json b/src/util/ipcTypes.json
index 7570d80..a53cd32 100644
--- a/src/util/ipcTypes.json
+++ b/src/util/ipcTypes.json
@@ -27,6 +27,16 @@
"watcher": {
"getpolling": "toMain_settings_watcher_getpolling",
"setpolling": "toMain_settings_watcher_setpolling"
+ },
+ "paintScale": {
+ "getInputConfigs": "toMain_settings_paintScale_getInputConfigs",
+ "setInputConfigs": "toMain_settings_paintScale_setInputConfigs",
+ "setInputPath": "toMain_settings_paintScale_setInputPath",
+ "getOutputConfigs": "toMain_settings_paintScale_getOutputConfigs",
+ "setOutputConfigs": "toMain_settings_paintScale_setOutputConfigs",
+ "setOutputPath": "toMain_settings_paintScale_setOutputPath",
+ "updateInputCron": "toMain_settings_paintScale_updateInputCron",
+ "updateOutputCron": "toMain_settings_paintScale_updateOutputCron"
}
},
"user": {
@@ -58,4 +68,4 @@
"showErrorMessage": "toRenderer_general_showErrorMessage"
}
}
-}
+}
\ No newline at end of file
diff --git a/src/util/translations/en-US/renderer.json b/src/util/translations/en-US/renderer.json
index d7f2abf..80c7f64 100644
--- a/src/util/translations/en-US/renderer.json
+++ b/src/util/translations/en-US/renderer.json
@@ -25,6 +25,9 @@
"startwatcher": "Start Watcher",
"stopwatcher": "Stop Watcher\n"
},
+ "errors": {
+ "duplicatePath": "The selected directory is already used in another configuration."
+ },
"labels": {
"emsOutFilePath": "EMS Out File Path (Parts Order, etc.)",
"pollinginterval": "Polling Interval (ms)",
@@ -34,7 +37,18 @@
"watchedpaths": "Watched Paths",
"watchermodepolling": "Polling",
"watchermoderealtime": "Real Time",
- "watcherstatus": "Watcher Status"
+ "watcherstatus": "Watcher Status",
+ "paintScaleSettingsInput": "BSMS To Paint Scale",
+ "paintScaleSettingsOutput": "Paint Scale To BSMS",
+ "paintScalePath": "Paint Scale Path",
+ "paintScaleType": "Paint Scale Type",
+ "addPaintScalePath": "Add Paint Scale Path",
+ "remove": "Remove",
+ "actions": "Actions",
+ "pollingInterval": "Polling Interval (m)",
+ "validPath": "Valid path",
+ "invalidPath": "Path not set or invalid",
+ "selectPaintScaleType": "Select Paint Scale Type"
}
},
"title": {
diff --git a/src/util/types/paintScale.ts b/src/util/types/paintScale.ts
new file mode 100644
index 0000000..a3fd053
--- /dev/null
+++ b/src/util/types/paintScale.ts
@@ -0,0 +1,17 @@
+export enum PaintScaleType {
+ PPG = "PPG",
+}
+
+export interface PaintScaleConfig {
+ id: string;
+ path?: string;
+ type: PaintScaleType;
+ pollingInterval: number;
+}
+
+export const paintScaleTypeOptions = Object.values(PaintScaleType).map(
+ (type) => ({
+ value: type,
+ label: type,
+ }),
+);