Compare commits
11 Commits
feature/IO
...
release/1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fa97a081f | ||
|
|
be0eafec7e | ||
|
|
6e4c8b5558 | ||
|
|
fb106ec87f | ||
|
|
2a5c2b43e1 | ||
|
|
eab52bf8c1 | ||
|
|
cd5ddc4fa1 | ||
|
|
3f2501cd90 | ||
|
|
019e1b161b | ||
|
|
cdad47b82f | ||
|
|
2953aa420e |
@@ -6,4 +6,4 @@ 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="http://api.test.imex.online"
|
||||
VITE_API_TEST_URL="https://api.test.imex.online"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,5 +14,6 @@ out
|
||||
|
||||
# Build Files
|
||||
macbuild.sh
|
||||
deploy.ps1
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@@ -13,7 +13,8 @@
|
||||
"runtimeArgs": ["--sourcemap"],
|
||||
"env": {
|
||||
"REMOTE_DEBUGGING_PORT": "9222"
|
||||
}
|
||||
},
|
||||
"experimentalNetworking": "off"
|
||||
},
|
||||
{
|
||||
"name": "Debug Renderer Process",
|
||||
|
||||
17
build/com.convenient-brands.bodyshop-desktop.keepalive.plist
Normal file
17
build/com.convenient-brands.bodyshop-desktop.keepalive.plist
Normal 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>
|
||||
@@ -23,7 +23,7 @@ win:
|
||||
certificateProfileName: ImEXRPS
|
||||
codeSigningAccountName: ImEX
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
artifactName: imex-partner-${arch}.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
@@ -49,7 +49,7 @@ mac:
|
||||
arch:
|
||||
- x64
|
||||
dmg:
|
||||
artifactName: ${name}-${version}-${arch}.${ext}
|
||||
artifactName: imex-partner-${arch}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
@@ -59,7 +59,7 @@ linux:
|
||||
category: Utility
|
||||
desktop: scripts/imex-shop-partner.desktop
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
artifactName: imex-partner-${arch}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: s3
|
||||
|
||||
@@ -23,7 +23,7 @@ win:
|
||||
certificateProfileName: ImEXRPS
|
||||
codeSigningAccountName: ImEX
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
artifactName: rome-partner-${arch}.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
@@ -49,7 +49,7 @@ mac:
|
||||
arch:
|
||||
- x64
|
||||
dmg:
|
||||
artifactName: ${name}-${version}-${arch}.${ext}
|
||||
artifactName: rome-partner-${arch}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
@@ -59,7 +59,7 @@ linux:
|
||||
category: Utility
|
||||
desktop: scripts/rome-shop-partner.desktop
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
artifactName: rome-partner-${arch}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: s3
|
||||
|
||||
796
package-lock.json
generated
796
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bodyshop-desktop",
|
||||
"version": "0.0.1-alpha.8",
|
||||
"version": "1.0.1",
|
||||
"description": "Shop Management System Partner",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Convenient Brands, LLC",
|
||||
@@ -55,6 +55,7 @@
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"antd": "^5.24.6",
|
||||
"archiver": "^7.0.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dbffile": "^1.12.0",
|
||||
@@ -72,6 +73,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",
|
||||
@@ -84,7 +86,6 @@
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "6.2.6",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^3.1.1",
|
||||
"node-cron": "^3.0.3"
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ 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);
|
||||
@@ -191,6 +192,14 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
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({
|
||||
|
||||
104
src/main/decoder/emsbackup.ts
Normal file
104
src/main/decoder/emsbackup.ts
Normal 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;
|
||||
@@ -19,6 +19,8 @@ const EmsPartsOrderGenerateLinFile = async (
|
||||
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
|
||||
|
||||
@@ -24,7 +24,7 @@ const EmsPartsOrderGenerateTtlFile = async (
|
||||
ttlFieldLineDescriptors,
|
||||
);
|
||||
|
||||
await dbf.appendRecords(records);
|
||||
await dbf.appendRecords([records]);
|
||||
console.log(`${records.length} TTL file records added.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface Jobline {
|
||||
bett_type: string | null;
|
||||
cert_part: boolean;
|
||||
est_seq: string | null;
|
||||
part_descj: boolean;
|
||||
}
|
||||
|
||||
// Parts Order Line export interface
|
||||
|
||||
@@ -2,6 +2,7 @@ import { UUID } from "crypto";
|
||||
import { parse, TypedQueryDocumentNode } from "graphql";
|
||||
import { gql } from "graphql-request";
|
||||
import { AvailableJobSchema } from "../decoder/decoder";
|
||||
|
||||
// Define types for the query result and variables
|
||||
export interface ActiveBodyshopQueryResult {
|
||||
bodyshops: Array<{
|
||||
@@ -11,9 +12,8 @@ export interface ActiveBodyshopQueryResult {
|
||||
convenient_company: string;
|
||||
}>;
|
||||
}
|
||||
// No variables needed for this query
|
||||
|
||||
// Transform the string query into a TypedQueryDocumentNode
|
||||
// No variables needed for this query
|
||||
export const QUERY_ACTIVE_BODYSHOP_TYPED: TypedQueryDocumentNode<
|
||||
ActiveBodyshopQueryResult,
|
||||
Record<never, never>
|
||||
@@ -34,9 +34,11 @@ export interface MasterdataQueryResult {
|
||||
key: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface MasterdataQueryVariables {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode<
|
||||
MasterdataQueryResult,
|
||||
MasterdataQueryVariables
|
||||
@@ -54,9 +56,11 @@ export interface VehicleQueryResult {
|
||||
id: UUID;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface VehicleQueryVariables {
|
||||
vin: string;
|
||||
}
|
||||
|
||||
export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode<
|
||||
VehicleQueryResult,
|
||||
VehicleQueryVariables
|
||||
@@ -73,9 +77,11 @@ export interface QueryJobByClmNoResult {
|
||||
id: UUID;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface QueryJobByClmNoVariables {
|
||||
clm_no: string;
|
||||
}
|
||||
|
||||
export const QUERY_JOB_BY_CLM_NO_TYPED: TypedQueryDocumentNode<
|
||||
QueryJobByClmNoResult,
|
||||
QueryJobByClmNoVariables
|
||||
@@ -92,9 +98,11 @@ export interface InsertAvailableJobResult {
|
||||
id: UUID;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface InsertAvailableJobVariables {
|
||||
jobInput: Array<AvailableJobSchema>;
|
||||
}
|
||||
|
||||
export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
|
||||
InsertAvailableJobResult,
|
||||
InsertAvailableJobVariables
|
||||
@@ -125,3 +133,140 @@ export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
|
||||
InsertAvailableJobResult,
|
||||
InsertAvailableJobVariables
|
||||
>;
|
||||
|
||||
// Add PpgData Query
|
||||
export interface PpgDataQueryResult {
|
||||
bodyshops_by_pk: {
|
||||
id: string;
|
||||
shopname: string;
|
||||
imexshopid: string;
|
||||
} | null;
|
||||
jobs: Array<{
|
||||
id: string;
|
||||
ro_number: string;
|
||||
status: string;
|
||||
ownr_fn: string;
|
||||
ownr_ln: string;
|
||||
ownr_co_nm: string;
|
||||
v_vin: string;
|
||||
v_model_yr: string;
|
||||
v_make_desc: string;
|
||||
v_model_desc: string;
|
||||
v_color: string;
|
||||
plate_no: string;
|
||||
ins_co_nm: string;
|
||||
est_ct_fn: string;
|
||||
est_ct_ln: string;
|
||||
rate_mapa: number;
|
||||
rate_lab: number;
|
||||
job_totals: {
|
||||
rates?: {
|
||||
mapa?: {
|
||||
total?: {
|
||||
amount?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
totals?: {
|
||||
subtotal?: {
|
||||
amount?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
vehicle: {
|
||||
v_paint_codes: {
|
||||
paint_cd1?: string;
|
||||
};
|
||||
};
|
||||
labhrs: {
|
||||
aggregate: {
|
||||
sum: {
|
||||
mod_lb_hrs: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
larhrs: {
|
||||
aggregate: {
|
||||
sum: {
|
||||
mod_lb_hrs: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface PpgDataQueryVariables {
|
||||
today: string;
|
||||
todayplus5: string;
|
||||
shopid: string;
|
||||
}
|
||||
|
||||
export const PPG_DATA_QUERY_TYPED: TypedQueryDocumentNode<
|
||||
PpgDataQueryResult,
|
||||
PpgDataQueryVariables
|
||||
> = parse(gql`
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`) as TypedQueryDocumentNode<PpgDataQueryResult, PpgDataQueryVariables>;
|
||||
|
||||
@@ -31,6 +31,8 @@ export default class LocalServer {
|
||||
"https://localhost:3000",
|
||||
"https://test.imex.online",
|
||||
"https://imex.online",
|
||||
"https://test.romeonline.io",
|
||||
"https://romeonline.io",
|
||||
];
|
||||
|
||||
this.app.use(
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
isKeepAliveTaskInstalled,
|
||||
setupKeepAliveTask,
|
||||
} from "./setup-keep-alive-task";
|
||||
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
|
||||
@@ -56,11 +57,14 @@ function createWindow(): void {
|
||||
y: number | undefined;
|
||||
};
|
||||
|
||||
// Validate window position is on screen
|
||||
const { validX, validY } = ensureWindowOnScreen(x, y, width, height);
|
||||
|
||||
const mainWindow = new BrowserWindow({
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
x: validX,
|
||||
y: validY,
|
||||
show: false, // Start hidden, show later if not keep-alive
|
||||
minWidth: 600,
|
||||
minHeight: 400,
|
||||
@@ -464,27 +468,32 @@ app.whenReady().then(async () => {
|
||||
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
|
||||
return;
|
||||
} else {
|
||||
openInExplorer(url);
|
||||
}
|
||||
} else {
|
||||
openMainWindow(); // Focus window if no URL
|
||||
}
|
||||
// No action taken if no URL is provided
|
||||
});
|
||||
|
||||
//Dynamically load ipcMain handlers once ready.
|
||||
try {
|
||||
const module = await import("./ipc/ipcMainConfig");
|
||||
const { initializeCronTasks } = await import("./ipc/ipcMainConfig");
|
||||
log.debug("Successfully loaded ipcMainConfig");
|
||||
|
||||
// Initialize cron tasks after loading ipcMainConfig
|
||||
await module.initializeCronTasks();
|
||||
log.info("Cron tasks initialized successfully");
|
||||
try {
|
||||
await initializeCronTasks();
|
||||
log.info("Cron tasks initialized successfully");
|
||||
} catch (error) {
|
||||
log.warn("Non-fatal: Failed to initialize cron tasks", {
|
||||
...ErrorTypeCheck(error),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to load ipcMainConfig or initialize cron tasks", {
|
||||
log.error("Fatal: Failed to load ipcMainConfig", {
|
||||
...ErrorTypeCheck(error),
|
||||
});
|
||||
throw error; // Adjust based on whether the app should continue
|
||||
}
|
||||
|
||||
//Create Tray
|
||||
@@ -568,15 +577,10 @@ 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.
|
||||
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
|
||||
// Do nothing, whether app is running or not
|
||||
return;
|
||||
} else {
|
||||
openInExplorer(url);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} 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", []);
|
||||
@@ -35,8 +34,8 @@ const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -45,12 +44,12 @@ const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
|
||||
};
|
||||
|
||||
const SettingsWatchedFilePathsRemove = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
path: string,
|
||||
_event: IpcMainInvokeEvent,
|
||||
path: string,
|
||||
): Promise<string[]> => {
|
||||
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");
|
||||
@@ -65,16 +64,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;
|
||||
@@ -131,11 +130,13 @@ const SettingEmsOutFilePathSet = async (): Promise<string> => {
|
||||
return (Store.get("settings.emsOutFilePath") as string) || "";
|
||||
};
|
||||
|
||||
const SettingsPaintScaleInputConfigsGet = async (
|
||||
_event?: IpcMainInvokeEvent,
|
||||
): Promise<PaintScaleConfig[]> => {
|
||||
const SettingsPaintScaleInputConfigsGet = (
|
||||
_event?: IpcMainInvokeEvent,
|
||||
): PaintScaleConfig[] => {
|
||||
try {
|
||||
const configs = Store.get("settings.paintScaleInputConfigs") as PaintScaleConfig[];
|
||||
const configs = Store.get(
|
||||
"settings.paintScaleInputConfigs",
|
||||
) as PaintScaleConfig[];
|
||||
log.debug("Retrieved paint scale input configs:", configs);
|
||||
return configs || [];
|
||||
} catch (error) {
|
||||
@@ -145,8 +146,8 @@ const SettingsPaintScaleInputConfigsGet = async (
|
||||
};
|
||||
|
||||
const SettingsPaintScaleInputConfigsSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
configs: PaintScaleConfig[],
|
||||
_event: IpcMainInvokeEvent,
|
||||
configs: PaintScaleConfig[],
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
Store.set("settings.paintScaleInputConfigs", configs);
|
||||
@@ -159,7 +160,7 @@ const SettingsPaintScaleInputConfigsSet = async (
|
||||
};
|
||||
|
||||
const SettingsPaintScaleInputPathSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
_event: IpcMainInvokeEvent,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const mainWindow = getMainWindow();
|
||||
@@ -183,11 +184,13 @@ const SettingsPaintScaleInputPathSet = async (
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsPaintScaleOutputConfigsGet = async (
|
||||
_event?: IpcMainInvokeEvent,
|
||||
): Promise<PaintScaleConfig[]> => {
|
||||
const SettingsPaintScaleOutputConfigsGet = (
|
||||
_event?: IpcMainInvokeEvent,
|
||||
): PaintScaleConfig[] => {
|
||||
try {
|
||||
const configs = Store.get("settings.paintScaleOutputConfigs") as PaintScaleConfig[];
|
||||
const configs = Store.get(
|
||||
"settings.paintScaleOutputConfigs",
|
||||
) as PaintScaleConfig[];
|
||||
log.debug("Retrieved paint scale output configs:", configs);
|
||||
return configs || [];
|
||||
} catch (error) {
|
||||
@@ -197,8 +200,8 @@ const SettingsPaintScaleOutputConfigsGet = async (
|
||||
};
|
||||
|
||||
const SettingsPaintScaleOutputConfigsSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
configs: PaintScaleConfig[],
|
||||
_event: IpcMainInvokeEvent,
|
||||
configs: PaintScaleConfig[],
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
Store.set("settings.paintScaleOutputConfigs", configs);
|
||||
@@ -211,7 +214,7 @@ const SettingsPaintScaleOutputConfigsSet = async (
|
||||
};
|
||||
|
||||
const SettingsPaintScaleOutputPathSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
_event: IpcMainInvokeEvent,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
@@ -26,11 +26,11 @@ const ipcMainHandleAuthStateChanged = async (
|
||||
log.debug("Received authentication state change from Renderer.", user);
|
||||
handleShopMetaDataFetch();
|
||||
//Check for updates
|
||||
const convCo = Store.get("app.bodyshop");
|
||||
if (convCo === "alpha") {
|
||||
const bodyshop = Store.get("app.bodyshop");
|
||||
if (bodyshop?.convenient_company === "alpha") {
|
||||
autoUpdater.channel = "alpha";
|
||||
log.debug("Setting update channel to ALPHA channel.");
|
||||
} else if (convCo === "beta") {
|
||||
} else if (bodyshop?.convenient_company === "beta") {
|
||||
autoUpdater.channel = "beta";
|
||||
log.debug("Setting update channel to BETA channel.");
|
||||
}
|
||||
|
||||
@@ -5,24 +5,43 @@ import axios from "axios";
|
||||
import { create } from "xmlbuilder2";
|
||||
import { parseStringPromise } from "xml2js";
|
||||
import store from "../../store/store";
|
||||
import client from "../../graphql/graphql-client";
|
||||
import client, { getTokenFromRenderer } from "../../graphql/graphql-client";
|
||||
import { PaintScaleConfig } from "../../../util/types/paintScale";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
PPG_DATA_QUERY_TYPED,
|
||||
PpgDataQueryResult,
|
||||
PpgDataQueryVariables,
|
||||
} from "../../graphql/queries";
|
||||
|
||||
// PPG Input Handler
|
||||
export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
||||
try {
|
||||
log.info(
|
||||
`Polling input directory for PPG config ${config.id}: ${config.path}`,
|
||||
);
|
||||
|
||||
log.debug(
|
||||
`Archive dir: ${path.join(config.path!, "archive")}, Error dir: ${path.join(config.path!, "error")}`,
|
||||
);
|
||||
|
||||
// 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 });
|
||||
try {
|
||||
await fs.mkdir(archiveDir, { recursive: true });
|
||||
await fs.mkdir(errorDir, { recursive: true });
|
||||
log.debug(
|
||||
`Archive and error directories ensured: ${archiveDir}, ${errorDir}`,
|
||||
);
|
||||
} catch (dirError) {
|
||||
log.error(`Failed to create directories for ${config.path}:`, dirError);
|
||||
throw dirError;
|
||||
}
|
||||
|
||||
// Check for files
|
||||
const files = await fs.readdir(config.path!);
|
||||
log.debug(`Found ${files.length} files in ${config.path}:`, files);
|
||||
|
||||
for (const file of files) {
|
||||
// Only process XML files
|
||||
if (!file.toLowerCase().endsWith(".xml")) {
|
||||
@@ -30,8 +49,13 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
||||
}
|
||||
|
||||
const filePath = path.join(config.path!, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (!stats.isFile()) {
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
if (!stats.isFile()) {
|
||||
continue;
|
||||
}
|
||||
} catch (statError) {
|
||||
log.warn(`Failed to stat file ${filePath}:`, statError);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -46,40 +70,61 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
||||
}
|
||||
|
||||
// Validate XML structure
|
||||
let xmlContent : BlobPart;
|
||||
|
||||
let xmlContent: BlobPart;
|
||||
try {
|
||||
xmlContent = await fs.readFile(filePath, "utf8");
|
||||
await parseStringPromise(xmlContent);
|
||||
log.debug(`Successfully validated XML for ${filePath}`);
|
||||
} 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}`);
|
||||
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
||||
const originalFilename = path.basename(file, path.extname(file));
|
||||
const errorPath = path.join(
|
||||
errorDir,
|
||||
`${originalFilename}-${timestamp}.xml`,
|
||||
);
|
||||
try {
|
||||
await fs.rename(filePath, errorPath);
|
||||
log.debug(`Moved invalid file to error: ${errorPath}`);
|
||||
} catch (moveError) {
|
||||
log.error(
|
||||
`Failed to move invalid file to error directory ${errorPath}:`,
|
||||
moveError,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get authentication token
|
||||
const token = (store.get("user") as any)?.stsTokenManager?.accessToken;
|
||||
if (!token) {
|
||||
log.error(`No authentication token for file: ${filePath}`);
|
||||
let token: string | null;
|
||||
try {
|
||||
token = await getTokenFromRenderer();
|
||||
if (!token) {
|
||||
log.error(`No authentication token for file: ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
log.debug(
|
||||
`Obtained authentication token for ${filePath}: ${token.slice(0, 10)}...`,
|
||||
);
|
||||
} catch (tokenError) {
|
||||
log.error(
|
||||
`Failed to obtain authentication token for ${filePath}:`,
|
||||
tokenError,
|
||||
);
|
||||
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 shopId = (store.get("app.bodyshop") as any)?.shopname || "";
|
||||
formData.append("shopId", shopId);
|
||||
log.debug(`Shop ID: ${shopId}`);
|
||||
|
||||
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 {
|
||||
@@ -88,23 +133,52 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
timeout: 10000, // 10-second timeout
|
||||
});
|
||||
|
||||
log.info(`Upload response for ${filePath}:`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.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}`);
|
||||
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
||||
const originalFilename = path.basename(file, path.extname(file));
|
||||
const archivePath = path.join(
|
||||
archiveDir,
|
||||
`${originalFilename}-${timestamp}.xml`,
|
||||
);
|
||||
try {
|
||||
await fs.access(archiveDir, fs.constants.W_OK); // Verify archiveDir is writable
|
||||
await fs.rename(filePath, archivePath);
|
||||
log.info(`Moved file to archive: ${archivePath}`);
|
||||
} catch (moveError) {
|
||||
log.error(
|
||||
`Failed to move file to archive directory ${archivePath}:`,
|
||||
moveError,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.error(
|
||||
`Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
|
||||
response.data,
|
||||
{ responseData: response.data },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Error uploading ${filePath}:`, error);
|
||||
} catch (error: any) {
|
||||
log.error(`Error uploading ${filePath}:`, {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response
|
||||
? {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
data: error.response.data,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -121,70 +195,16 @@ export async function ppgOutputHandler(
|
||||
|
||||
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(),
|
||||
const variables: PpgDataQueryVariables = {
|
||||
today: dayjs().toISOString(),
|
||||
todayplus5: dayjs().add(5, "day").toISOString(),
|
||||
shopid: (store.get("app.bodyshop") as any)?.id,
|
||||
};
|
||||
|
||||
const response = (await client.request(query, variables)) as any;
|
||||
const response = await client.request<
|
||||
PpgDataQueryResult,
|
||||
PpgDataQueryVariables
|
||||
>(PPG_DATA_QUERY_TYPED, variables);
|
||||
const jobs = response.jobs ?? [];
|
||||
|
||||
const header = {
|
||||
@@ -197,18 +217,10 @@ export async function ppgOutputHandler(
|
||||
},
|
||||
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}`;
|
||||
})(),
|
||||
TransactionDate: dayjs().format("YYYY-MM-DD:HH:mm"),
|
||||
},
|
||||
Product: {
|
||||
Name: "ImEX Online",
|
||||
Name: import.meta.env.VITE_COMPANY === "IMEX",
|
||||
Version: "",
|
||||
},
|
||||
},
|
||||
@@ -220,7 +232,7 @@ export async function ppgOutputHandler(
|
||||
},
|
||||
RepairOrders: {
|
||||
ROCount: jobs.length.toString(),
|
||||
RO: jobs.map((job: any) => ({
|
||||
RO: jobs.map((job) => ({
|
||||
RONumber: job.ro_number || "",
|
||||
ROStatus: "Open",
|
||||
Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`,
|
||||
@@ -258,4 +270,3 @@ export async function ppgOutputHandler(
|
||||
log.error(`Error generating PPG output for config ${config.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function setupKeepAliveAgent(): Promise<void> {
|
||||
<string>com.convenientbrands.bodyshop-desktop.keepalive</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>open</string>
|
||||
<string>Shop Partner Keep Alive</string>
|
||||
<string>imexmedia://keep-alive</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
@@ -30,8 +30,8 @@ export async function setupKeepAliveAgent(): Promise<void> {
|
||||
</plist>`;
|
||||
|
||||
const plistPath = join(
|
||||
homedir(),
|
||||
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||
homedir(),
|
||||
"/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -40,15 +40,17 @@ export async function setupKeepAliveAgent(): Promise<void> {
|
||||
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)}`);
|
||||
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<boolean> {
|
||||
const plistPath = join(
|
||||
homedir(),
|
||||
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||
homedir(),
|
||||
"/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
|
||||
);
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 500; // 500ms delay between retries
|
||||
@@ -56,10 +58,14 @@ export async function isKeepAliveAgentInstalled(): Promise<boolean> {
|
||||
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`);
|
||||
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)}`);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ const execPromise = promisify(exec);
|
||||
|
||||
// Define the interval as a variable (in minutes)
|
||||
const KEEP_ALIVE_INTERVAL_MINUTES = 15;
|
||||
const taskName = "ShopPartnerKeepAlive";
|
||||
|
||||
export async function setupKeepAliveTask(): Promise<void> {
|
||||
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}"`;
|
||||
@@ -30,7 +30,6 @@ export async function setupKeepAliveTask(): Promise<void> {
|
||||
}
|
||||
|
||||
export async function isKeepAliveTaskInstalled(): Promise<boolean> {
|
||||
const taskName = "ImEXShopPartnerKeepAlive";
|
||||
const maxRetries = 3;
|
||||
const retryDelay = 500; // 500ms delay between retries
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ttlFieldLineDescriptors: FieldDescriptor[] = [
|
||||
decimalPlaces: 2,
|
||||
},
|
||||
{
|
||||
name: "N_SUPP_ANT",
|
||||
name: "N_SUPP_AMT",
|
||||
type: "N",
|
||||
size: 10,
|
||||
decimalPlaces: 2,
|
||||
|
||||
109
src/main/util/ensureWindowOnScreen.ts
Normal file
109
src/main/util/ensureWindowOnScreen.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { screen } from "electron";
|
||||
|
||||
function ensureWindowOnScreen(
|
||||
x: number | undefined,
|
||||
y: number | undefined,
|
||||
windowWidth: number,
|
||||
windowHeight: number,
|
||||
): { validX: number | undefined; validY: number | undefined } {
|
||||
// If no coordinates stored, let Electron position window automatically
|
||||
if (x === undefined || y === undefined) {
|
||||
return { validX: undefined, validY: undefined };
|
||||
}
|
||||
|
||||
const displays = screen.getAllDisplays();
|
||||
|
||||
// Minimum visible pixels required on each edge to be considered "visible enough"
|
||||
const MIN_VISIBLE = 50; // Ensure at least 50px from each edge is visible
|
||||
|
||||
// Try to find a display where the window would be almost fully visible
|
||||
for (const display of displays) {
|
||||
const { bounds } = display;
|
||||
|
||||
// Check if window is mostly within this display
|
||||
if (
|
||||
x + MIN_VISIBLE >= bounds.x &&
|
||||
x + windowWidth - MIN_VISIBLE <= bounds.x + bounds.width &&
|
||||
y + MIN_VISIBLE >= bounds.y &&
|
||||
y + windowHeight - MIN_VISIBLE <= bounds.y + bounds.height
|
||||
) {
|
||||
// Window is adequately visible on this display
|
||||
return { validX: x, validY: y };
|
||||
}
|
||||
}
|
||||
|
||||
// If window isn't adequately visible on any display, try to adjust it to fit the closest display
|
||||
const closestDisplay = findClosestDisplay(displays, x, y);
|
||||
const { bounds } = closestDisplay;
|
||||
|
||||
// Adjust position to ensure window is fully on screen
|
||||
let adjustedX = x;
|
||||
let adjustedY = y;
|
||||
|
||||
// Adjust horizontal position if needed
|
||||
if (x < bounds.x) {
|
||||
adjustedX = bounds.x;
|
||||
} else if (x + windowWidth > bounds.x + bounds.width) {
|
||||
adjustedX = bounds.x + bounds.width - windowWidth;
|
||||
}
|
||||
|
||||
// Adjust vertical position if needed
|
||||
if (y < bounds.y) {
|
||||
adjustedY = bounds.y;
|
||||
} else if (y + windowHeight > bounds.y + bounds.height) {
|
||||
adjustedY = bounds.y + bounds.height - windowHeight;
|
||||
}
|
||||
|
||||
// If adjustments keep window on screen, use adjusted position
|
||||
if (
|
||||
adjustedX >= bounds.x &&
|
||||
adjustedX + windowWidth <= bounds.x + bounds.width &&
|
||||
adjustedY >= bounds.y &&
|
||||
adjustedY + windowHeight <= bounds.y + bounds.height
|
||||
) {
|
||||
return { validX: adjustedX, validY: adjustedY };
|
||||
}
|
||||
|
||||
// If all else fails, center on primary display
|
||||
const primaryDisplay = screen.getPrimaryDisplay();
|
||||
const primaryBounds = primaryDisplay.bounds;
|
||||
|
||||
return {
|
||||
validX: Math.floor(
|
||||
primaryBounds.x + (primaryBounds.width - windowWidth) / 2,
|
||||
),
|
||||
validY: Math.floor(
|
||||
primaryBounds.y + (primaryBounds.height - windowHeight) / 2,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to find the closest display to a point
|
||||
function findClosestDisplay(
|
||||
displays: Electron.Display[],
|
||||
x: number,
|
||||
y: number,
|
||||
): Electron.Display {
|
||||
let closestDisplay = displays[0];
|
||||
let shortestDistance = Number.MAX_VALUE;
|
||||
|
||||
for (const display of displays) {
|
||||
const { bounds } = display;
|
||||
// Calculate distance to center of display
|
||||
const displayCenterX = bounds.x + bounds.width / 2;
|
||||
const displayCenterY = bounds.y + bounds.height / 2;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(x - displayCenterX, 2) + Math.pow(y - displayCenterY, 2),
|
||||
);
|
||||
|
||||
if (distance < shortestDistance) {
|
||||
shortestDistance = distance;
|
||||
closestDisplay = display;
|
||||
}
|
||||
}
|
||||
|
||||
return closestDisplay;
|
||||
}
|
||||
|
||||
export default ensureWindowOnScreen;
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
theme,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { FC, useState } from "react";
|
||||
import { JSX, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
PaintScaleConfig,
|
||||
@@ -24,8 +25,10 @@ import {
|
||||
} from "../../../../util/types/paintScale";
|
||||
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
||||
|
||||
const SettingsPaintScaleInputPaths: FC = () => {
|
||||
const SettingsPaintScaleInputPaths = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const { token } = theme.useToken(); // Access theme tokens
|
||||
|
||||
const {
|
||||
paintScaleConfigs,
|
||||
handleAddConfig,
|
||||
@@ -87,7 +90,7 @@ const SettingsPaintScaleInputPaths: FC = () => {
|
||||
placeholder={t("settings.labels.paintScalePath")}
|
||||
disabled
|
||||
style={{
|
||||
borderColor: isValid ? "#52c41a" : "#d9d9d9",
|
||||
borderColor: isValid ? token.colorSuccess : token.colorError, // Use semantic tokens
|
||||
}}
|
||||
suffix={
|
||||
<Tooltip
|
||||
@@ -98,9 +101,9 @@ const SettingsPaintScaleInputPaths: FC = () => {
|
||||
}
|
||||
>
|
||||
{isValid ? (
|
||||
<CheckCircleFilled style={{ color: "#52c41a" }} />
|
||||
<CheckCircleFilled style={{ color: token.colorSuccess }} />
|
||||
) : (
|
||||
<WarningFilled style={{ color: "#faad14" }} />
|
||||
<WarningFilled style={{ color: token.colorError }} />
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
@@ -4,8 +4,18 @@ import {
|
||||
FolderOpenFilled,
|
||||
WarningFilled,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Card, Input, Modal, Select, Space, Table, Tag } from "antd";
|
||||
import { FC, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
theme,
|
||||
} from "antd";
|
||||
import { JSX, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
PaintScaleConfig,
|
||||
@@ -14,7 +24,8 @@ import {
|
||||
} from "../../../../util/types/paintScale";
|
||||
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
|
||||
|
||||
const SettingsPaintScaleOutputPaths: FC = () => {
|
||||
const SettingsPaintScaleOutputPaths = (): JSX.Element => {
|
||||
const { token } = theme.useToken();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
paintScaleConfigs,
|
||||
@@ -77,13 +88,13 @@ const SettingsPaintScaleOutputPaths: FC = () => {
|
||||
placeholder={t("settings.labels.paintScalePath")}
|
||||
disabled
|
||||
style={{
|
||||
borderColor: isValid ? "#52c41a" : "#d9d9d9",
|
||||
borderColor: isValid ? token.colorSuccess : token.colorError,
|
||||
}}
|
||||
suffix={
|
||||
isValid ? (
|
||||
<CheckCircleFilled style={{ color: "#52c41a" }} />
|
||||
<CheckCircleFilled style={{ color: token.colorSuccess }} />
|
||||
) : (
|
||||
<WarningFilled style={{ color: "#faad14" }} />
|
||||
<WarningFilled style={{ color: token.colorError }} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user