11 Commits

Author SHA1 Message Date
Patrick Fic
6fa97a081f Resolve CORS issues. 2025-05-20 15:40:15 -07:00
Patrick FIc
be0eafec7e Update artifact names. 2025-05-16 10:39:42 -07:00
Patrick Fic
6e4c8b5558 Merged in feature/IO-3066-1-scaffolding (pull request #2)
Major v1 release.
2025-05-16 17:17:38 +00:00
Patrick Fic
fb106ec87f Major release v1.0.0 2025-05-16 10:02:26 -07:00
Patrick Fic
2a5c2b43e1 Make build artifacts consistent. 2025-05-16 10:01:58 -07:00
Patrick FIc
eab52bf8c1 Ensure window stays on screen after external monitor removed. 2025-05-15 15:19:15 -07:00
Patrick Fic
cd5ddc4fa1 Add EMS upload for archiving. 2025-05-15 14:29:30 -07:00
Patrick FIc
3f2501cd90 Part order testing fixes. 2025-05-12 14:59:02 -07:00
Patrick Fic
019e1b161b Additional keepalive fixes. 2025-05-05 11:15:51 -07:00
Dave Richer
cdad47b82f feature/IO-3066-1-scaffolding: Fix some missing PR Notes around PPG 2025-05-05 10:37:42 -04:00
Dave Richer
2953aa420e Merged in feature/IO-3205-Paint-Scale-Integrations (pull request #1)
[DO NOT MERGE] Feature/IO-3205 Paint Scale Integrations

Approved-by: Patrick Fic
2025-05-02 17:45:38 +00:00
25 changed files with 1218 additions and 381 deletions

View File

@@ -6,4 +6,4 @@ VITE_COMPANY=IMEX
VITE_FE_URL=https://imex.online VITE_FE_URL=https://imex.online
VITE_FE_URL_TEST=https://test.imex.online VITE_FE_URL_TEST=https://test.imex.online
VITE_API_URL="http://localhost:4000" 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
View File

@@ -14,5 +14,6 @@ out
# Build Files # Build Files
macbuild.sh macbuild.sh
deploy.ps1
# Sentry Config File # Sentry Config File
.env.sentry-build-plugin .env.sentry-build-plugin

3
.vscode/launch.json vendored
View File

@@ -13,7 +13,8 @@
"runtimeArgs": ["--sourcemap"], "runtimeArgs": ["--sourcemap"],
"env": { "env": {
"REMOTE_DEBUGGING_PORT": "9222" "REMOTE_DEBUGGING_PORT": "9222"
} },
"experimentalNetworking": "off"
}, },
{ {
"name": "Debug Renderer Process", "name": "Debug Renderer Process",

View File

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

View File

@@ -23,7 +23,7 @@ win:
certificateProfileName: ImEXRPS certificateProfileName: ImEXRPS
codeSigningAccountName: ImEX codeSigningAccountName: ImEX
nsis: nsis:
artifactName: ${name}-${version}-setup.${ext} artifactName: imex-partner-${arch}.${ext}
shortcutName: ${productName} shortcutName: ${productName}
uninstallDisplayName: ${productName} uninstallDisplayName: ${productName}
createDesktopShortcut: always createDesktopShortcut: always
@@ -49,7 +49,7 @@ mac:
arch: arch:
- x64 - x64
dmg: dmg:
artifactName: ${name}-${version}-${arch}.${ext} artifactName: imex-partner-${arch}.${ext}
linux: linux:
target: target:
- AppImage - AppImage
@@ -59,9 +59,9 @@ linux:
category: Utility category: Utility
desktop: scripts/imex-shop-partner.desktop desktop: scripts/imex-shop-partner.desktop
appImage: appImage:
artifactName: ${name}-${version}.${ext} artifactName: imex-partner-${arch}.${ext}
npmRebuild: false npmRebuild: false
publish: publish:
provider: s3 provider: s3
bucket: imex-partner bucket: imex-partner
region: ca-central-1 region: ca-central-1

View File

@@ -23,7 +23,7 @@ win:
certificateProfileName: ImEXRPS certificateProfileName: ImEXRPS
codeSigningAccountName: ImEX codeSigningAccountName: ImEX
nsis: nsis:
artifactName: ${name}-${version}-setup.${ext} artifactName: rome-partner-${arch}.${ext}
shortcutName: ${productName} shortcutName: ${productName}
uninstallDisplayName: ${productName} uninstallDisplayName: ${productName}
createDesktopShortcut: always createDesktopShortcut: always
@@ -49,7 +49,7 @@ mac:
arch: arch:
- x64 - x64
dmg: dmg:
artifactName: ${name}-${version}-${arch}.${ext} artifactName: rome-partner-${arch}.${ext}
linux: linux:
target: target:
- AppImage - AppImage
@@ -59,9 +59,9 @@ linux:
category: Utility category: Utility
desktop: scripts/rome-shop-partner.desktop desktop: scripts/rome-shop-partner.desktop
appImage: appImage:
artifactName: ${name}-${version}.${ext} artifactName: rome-partner-${arch}.${ext}
npmRebuild: false npmRebuild: false
publish: publish:
provider: s3 provider: s3
bucket: rome-partner bucket: rome-partner
region: us-east-2 region: us-east-2

796
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "bodyshop-desktop", "name": "bodyshop-desktop",
"version": "0.0.1-alpha.8", "version": "1.0.1",
"description": "Shop Management System Partner", "description": "Shop Management System Partner",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "Convenient Brands, LLC", "author": "Convenient Brands, LLC",
@@ -55,6 +55,7 @@
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"antd": "^5.24.6", "antd": "^5.24.6",
"archiver": "^7.0.1",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"dbffile": "^1.12.0", "dbffile": "^1.12.0",
@@ -72,6 +73,7 @@
"graphql-request": "^7.1.2", "graphql-request": "^7.1.2",
"i18next": "^24.2.3", "i18next": "^24.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"node-cron": "^3.0.3",
"playwright": "^1.51.1", "playwright": "^1.51.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react": "^19.1.0", "react": "^19.1.0",
@@ -84,7 +86,6 @@
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "6.2.6", "vite": "6.2.6",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"xmlbuilder2": "^3.1.1", "xmlbuilder2": "^3.1.1"
"node-cron": "^3.0.3"
} }
} }

View File

@@ -43,6 +43,7 @@ import { DecodedTtl } from "./decode-ttl.interface";
import DecodeVeh from "./decode-veh"; import DecodeVeh from "./decode-veh";
import { DecodedVeh } from "./decode-veh.interface"; import { DecodedVeh } from "./decode-veh.interface";
import setAppProgressbar from "../util/setAppProgressBar"; import setAppProgressbar from "../util/setAppProgressBar";
import UploadEmsToS3 from "./emsbackup";
async function ImportJob(filepath: string): Promise<void> { async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath); const parsedFilePath = path.parse(filepath);
@@ -191,6 +192,14 @@ async function ImportJob(filepath: string): Promise<void> {
uploadNotification.show(); uploadNotification.show();
log.debug("Job inserted", insertRecordResult); 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) { } catch (error) {
log.error("Error encountered while decoding job. ", errorTypeCheck(error)); log.error("Error encountered while decoding job. ", errorTypeCheck(error));
const uploadNotificationFailure = new Notification({ const uploadNotificationFailure = new Notification({

View File

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

View File

@@ -19,6 +19,8 @@ const EmsPartsOrderGenerateLinFile = async (
DB_REF: partsOrderLine.jobline?.db_ref, DB_REF: partsOrderLine.jobline?.db_ref,
UNQ_SEQ: partsOrderLine.jobline?.unq_seq, UNQ_SEQ: partsOrderLine.jobline?.unq_seq,
WHO_PAYS: partsOrderLine.jobline?.who_pays, WHO_PAYS: partsOrderLine.jobline?.who_pays,
PART_DESCJ: partsOrderLine.jobline?.part_descj,
LINE_DESC: partsOrderLine.jobline?.line_desc, LINE_DESC: partsOrderLine.jobline?.line_desc,
PART_TYPE: PART_TYPE:
partsOrderLine.priceChange === true partsOrderLine.priceChange === true

View File

@@ -24,7 +24,7 @@ const EmsPartsOrderGenerateTtlFile = async (
ttlFieldLineDescriptors, ttlFieldLineDescriptors,
); );
await dbf.appendRecords(records); await dbf.appendRecords([records]);
console.log(`${records.length} TTL file records added.`); console.log(`${records.length} TTL file records added.`);
return true; return true;
} catch (error) { } catch (error) {

View File

@@ -79,6 +79,7 @@ export interface Jobline {
bett_type: string | null; bett_type: string | null;
cert_part: boolean; cert_part: boolean;
est_seq: string | null; est_seq: string | null;
part_descj: boolean;
} }
// Parts Order Line export interface // Parts Order Line export interface

View File

@@ -2,6 +2,7 @@ import { UUID } from "crypto";
import { parse, TypedQueryDocumentNode } from "graphql"; import { parse, TypedQueryDocumentNode } from "graphql";
import { gql } from "graphql-request"; import { gql } from "graphql-request";
import { AvailableJobSchema } from "../decoder/decoder"; import { AvailableJobSchema } from "../decoder/decoder";
// Define types for the query result and variables // Define types for the query result and variables
export interface ActiveBodyshopQueryResult { export interface ActiveBodyshopQueryResult {
bodyshops: Array<{ bodyshops: Array<{
@@ -11,9 +12,8 @@ export interface ActiveBodyshopQueryResult {
convenient_company: string; 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< export const QUERY_ACTIVE_BODYSHOP_TYPED: TypedQueryDocumentNode<
ActiveBodyshopQueryResult, ActiveBodyshopQueryResult,
Record<never, never> Record<never, never>
@@ -34,9 +34,11 @@ export interface MasterdataQueryResult {
key: string; key: string;
}>; }>;
} }
interface MasterdataQueryVariables { interface MasterdataQueryVariables {
key: string; key: string;
} }
export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode< export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode<
MasterdataQueryResult, MasterdataQueryResult,
MasterdataQueryVariables MasterdataQueryVariables
@@ -54,9 +56,11 @@ export interface VehicleQueryResult {
id: UUID; id: UUID;
}>; }>;
} }
interface VehicleQueryVariables { interface VehicleQueryVariables {
vin: string; vin: string;
} }
export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode< export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode<
VehicleQueryResult, VehicleQueryResult,
VehicleQueryVariables VehicleQueryVariables
@@ -73,9 +77,11 @@ export interface QueryJobByClmNoResult {
id: UUID; id: UUID;
}>; }>;
} }
export interface QueryJobByClmNoVariables { export interface QueryJobByClmNoVariables {
clm_no: string; clm_no: string;
} }
export const QUERY_JOB_BY_CLM_NO_TYPED: TypedQueryDocumentNode< export const QUERY_JOB_BY_CLM_NO_TYPED: TypedQueryDocumentNode<
QueryJobByClmNoResult, QueryJobByClmNoResult,
QueryJobByClmNoVariables QueryJobByClmNoVariables
@@ -92,9 +98,11 @@ export interface InsertAvailableJobResult {
id: UUID; id: UUID;
}>; }>;
} }
export interface InsertAvailableJobVariables { export interface InsertAvailableJobVariables {
jobInput: Array<AvailableJobSchema>; jobInput: Array<AvailableJobSchema>;
} }
export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode< export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
InsertAvailableJobResult, InsertAvailableJobResult,
InsertAvailableJobVariables InsertAvailableJobVariables
@@ -125,3 +133,140 @@ export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
InsertAvailableJobResult, InsertAvailableJobResult,
InsertAvailableJobVariables 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>;

View File

@@ -31,6 +31,8 @@ export default class LocalServer {
"https://localhost:3000", "https://localhost:3000",
"https://test.imex.online", "https://test.imex.online",
"https://imex.online", "https://imex.online",
"https://test.romeonline.io",
"https://romeonline.io",
]; ];
this.app.use( this.app.use(

View File

@@ -33,6 +33,7 @@ import {
isKeepAliveTaskInstalled, isKeepAliveTaskInstalled,
setupKeepAliveTask, setupKeepAliveTask,
} from "./setup-keep-alive-task"; } from "./setup-keep-alive-task";
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
Sentry.init({ Sentry.init({
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296", dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
@@ -56,11 +57,14 @@ function createWindow(): void {
y: number | undefined; y: number | undefined;
}; };
// Validate window position is on screen
const { validX, validY } = ensureWindowOnScreen(x, y, width, height);
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width, width,
height, height,
x, x: validX,
y, y: validY,
show: false, // Start hidden, show later if not keep-alive show: false, // Start hidden, show later if not keep-alive
minWidth: 600, minWidth: 600,
minHeight: 400, minHeight: 400,
@@ -464,27 +468,32 @@ app.whenReady().then(async () => {
if (url.startsWith(`${protocol}://keep-alive`)) { if (url.startsWith(`${protocol}://keep-alive`)) {
log.info("Keep-alive protocol received, app is already running."); log.info("Keep-alive protocol received, app is already running.");
// Do nothing if already running // Do nothing if already running
return; // Skip openMainWindow to avoid focusing the window return;
} else { } else {
openInExplorer(url); openInExplorer(url);
} }
} else {
openMainWindow(); // Focus window if no URL
} }
// No action taken if no URL is provided
}); });
//Dynamically load ipcMain handlers once ready. //Dynamically load ipcMain handlers once ready.
try { try {
const module = await import("./ipc/ipcMainConfig"); const { initializeCronTasks } = await import("./ipc/ipcMainConfig");
log.debug("Successfully loaded ipcMainConfig"); log.debug("Successfully loaded ipcMainConfig");
// Initialize cron tasks after loading ipcMainConfig try {
await module.initializeCronTasks(); await initializeCronTasks();
log.info("Cron tasks initialized successfully"); log.info("Cron tasks initialized successfully");
} catch (error) {
log.warn("Non-fatal: Failed to initialize cron tasks", {
...ErrorTypeCheck(error),
});
}
} catch (error) { } catch (error) {
log.error("Failed to load ipcMainConfig or initialize cron tasks", { log.error("Fatal: Failed to load ipcMainConfig", {
...ErrorTypeCheck(error), ...ErrorTypeCheck(error),
}); });
throw error; // Adjust based on whether the app should continue
} }
//Create Tray //Create Tray
@@ -568,15 +577,10 @@ app.whenReady().then(async () => {
app.on("open-url", (event: Electron.Event, url: string) => { app.on("open-url", (event: Electron.Event, url: string) => {
event.preventDefault(); event.preventDefault();
//Don't do anything for now. We just want to open the app.
if (url.startsWith(`${protocol}://keep-alive`)) { if (url.startsWith(`${protocol}://keep-alive`)) {
log.info("Keep-alive protocol received."); log.info("Keep-alive protocol received.");
if (BrowserWindow.getAllWindows().length === 0) { // Do nothing, whether app is running or not
isKeepAliveLaunch = true; return;
openMainWindow(); // Launch app if not running
}
// Do nothing if already running
return; // Skip openMainWindow to avoid focusing the window
} else { } else {
openInExplorer(url); openInExplorer(url);
} }

View File

@@ -12,7 +12,6 @@ import {
} from "../watcher/watcher"; } from "../watcher/watcher";
import { PaintScaleConfig } from "../../util/types/paintScale"; import { PaintScaleConfig } from "../../util/types/paintScale";
// Initialize paint scale input configs in store if not set // Initialize paint scale input configs in store if not set
if (!Store.get("settings.paintScaleInputConfigs")) { if (!Store.get("settings.paintScaleInputConfigs")) {
Store.set("settings.paintScaleInputConfigs", []); Store.set("settings.paintScaleInputConfigs", []);
@@ -35,8 +34,8 @@ const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
if (!result.canceled) { if (!result.canceled) {
Store.set( Store.set(
"settings.filepaths", "settings.filepaths",
_.union(result.filePaths, Store.get("settings.filepaths")), _.union(result.filePaths, Store.get("settings.filepaths")),
); );
addWatcherPath(result.filePaths); addWatcherPath(result.filePaths);
} }
@@ -45,12 +44,12 @@ const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
}; };
const SettingsWatchedFilePathsRemove = async ( const SettingsWatchedFilePathsRemove = async (
_event: IpcMainInvokeEvent, _event: IpcMainInvokeEvent,
path: string, path: string,
): Promise<string[]> => { ): Promise<string[]> => {
Store.set( Store.set(
"settings.filepaths", "settings.filepaths",
_.without(Store.get("settings.filepaths"), path), _.without(Store.get("settings.filepaths"), path),
); );
removeWatcherPath(path); removeWatcherPath(path);
return Store.get("settings.filepaths"); return Store.get("settings.filepaths");
@@ -65,16 +64,16 @@ const SettingsWatcherPollingGet = async (): Promise<{
interval: number; interval: number;
}> => { }> => {
const pollingEnabled: { enabled: boolean; interval: number } = const pollingEnabled: { enabled: boolean; interval: number } =
Store.get("settings.polling"); Store.get("settings.polling");
return { enabled: pollingEnabled.enabled, interval: pollingEnabled.interval }; return { enabled: pollingEnabled.enabled, interval: pollingEnabled.interval };
}; };
const SettingsWatcherPollingSet = async ( const SettingsWatcherPollingSet = async (
_event: IpcMainInvokeEvent, _event: IpcMainInvokeEvent,
pollingSettings: { pollingSettings: {
enabled: boolean; enabled: boolean;
interval: number; interval: number;
}, },
): Promise<{ ): Promise<{
enabled: boolean; enabled: boolean;
interval: number; interval: number;
@@ -131,11 +130,13 @@ const SettingEmsOutFilePathSet = async (): Promise<string> => {
return (Store.get("settings.emsOutFilePath") as string) || ""; return (Store.get("settings.emsOutFilePath") as string) || "";
}; };
const SettingsPaintScaleInputConfigsGet = async ( const SettingsPaintScaleInputConfigsGet = (
_event?: IpcMainInvokeEvent, _event?: IpcMainInvokeEvent,
): Promise<PaintScaleConfig[]> => { ): PaintScaleConfig[] => {
try { 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); log.debug("Retrieved paint scale input configs:", configs);
return configs || []; return configs || [];
} catch (error) { } catch (error) {
@@ -145,8 +146,8 @@ const SettingsPaintScaleInputConfigsGet = async (
}; };
const SettingsPaintScaleInputConfigsSet = async ( const SettingsPaintScaleInputConfigsSet = async (
_event: IpcMainInvokeEvent, _event: IpcMainInvokeEvent,
configs: PaintScaleConfig[], configs: PaintScaleConfig[],
): Promise<boolean> => { ): Promise<boolean> => {
try { try {
Store.set("settings.paintScaleInputConfigs", configs); Store.set("settings.paintScaleInputConfigs", configs);
@@ -159,7 +160,7 @@ const SettingsPaintScaleInputConfigsSet = async (
}; };
const SettingsPaintScaleInputPathSet = async ( const SettingsPaintScaleInputPathSet = async (
_event: IpcMainInvokeEvent, _event: IpcMainInvokeEvent,
): Promise<string | null> => { ): Promise<string | null> => {
try { try {
const mainWindow = getMainWindow(); const mainWindow = getMainWindow();
@@ -183,11 +184,13 @@ const SettingsPaintScaleInputPathSet = async (
} }
}; };
const SettingsPaintScaleOutputConfigsGet = async ( const SettingsPaintScaleOutputConfigsGet = (
_event?: IpcMainInvokeEvent, _event?: IpcMainInvokeEvent,
): Promise<PaintScaleConfig[]> => { ): PaintScaleConfig[] => {
try { 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); log.debug("Retrieved paint scale output configs:", configs);
return configs || []; return configs || [];
} catch (error) { } catch (error) {
@@ -197,8 +200,8 @@ const SettingsPaintScaleOutputConfigsGet = async (
}; };
const SettingsPaintScaleOutputConfigsSet = async ( const SettingsPaintScaleOutputConfigsSet = async (
_event: IpcMainInvokeEvent, _event: IpcMainInvokeEvent,
configs: PaintScaleConfig[], configs: PaintScaleConfig[],
): Promise<boolean> => { ): Promise<boolean> => {
try { try {
Store.set("settings.paintScaleOutputConfigs", configs); Store.set("settings.paintScaleOutputConfigs", configs);
@@ -211,7 +214,7 @@ const SettingsPaintScaleOutputConfigsSet = async (
}; };
const SettingsPaintScaleOutputPathSet = async ( const SettingsPaintScaleOutputPathSet = async (
_event: IpcMainInvokeEvent, _event: IpcMainInvokeEvent,
): Promise<string | null> => { ): Promise<string | null> => {
try { try {
const mainWindow = getMainWindow(); const mainWindow = getMainWindow();
@@ -251,4 +254,4 @@ export {
SettingsPaintScaleOutputConfigsGet, SettingsPaintScaleOutputConfigsGet,
SettingsPaintScaleOutputConfigsSet, SettingsPaintScaleOutputConfigsSet,
SettingsPaintScaleOutputPathSet, SettingsPaintScaleOutputPathSet,
}; };

View File

@@ -26,11 +26,11 @@ const ipcMainHandleAuthStateChanged = async (
log.debug("Received authentication state change from Renderer.", user); log.debug("Received authentication state change from Renderer.", user);
handleShopMetaDataFetch(); handleShopMetaDataFetch();
//Check for updates //Check for updates
const convCo = Store.get("app.bodyshop"); const bodyshop = Store.get("app.bodyshop");
if (convCo === "alpha") { if (bodyshop?.convenient_company === "alpha") {
autoUpdater.channel = "alpha"; autoUpdater.channel = "alpha";
log.debug("Setting update channel to ALPHA channel."); log.debug("Setting update channel to ALPHA channel.");
} else if (convCo === "beta") { } else if (bodyshop?.convenient_company === "beta") {
autoUpdater.channel = "beta"; autoUpdater.channel = "beta";
log.debug("Setting update channel to BETA channel."); log.debug("Setting update channel to BETA channel.");
} }

View File

@@ -5,24 +5,43 @@ import axios from "axios";
import { create } from "xmlbuilder2"; import { create } from "xmlbuilder2";
import { parseStringPromise } from "xml2js"; import { parseStringPromise } from "xml2js";
import store from "../../store/store"; 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 { 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> { export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
try { try {
log.info( log.info(
`Polling input directory for PPG config ${config.id}: ${config.path}`, `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 // Ensure archive and error directories exist
const archiveDir = path.join(config.path!, "archive"); const archiveDir = path.join(config.path!, "archive");
const errorDir = path.join(config.path!, "error"); const errorDir = path.join(config.path!, "error");
await fs.mkdir(archiveDir, { recursive: true }); try {
await fs.mkdir(errorDir, { recursive: true }); 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 // Check for files
const files = await fs.readdir(config.path!); const files = await fs.readdir(config.path!);
log.debug(`Found ${files.length} files in ${config.path}:`, files);
for (const file of files) { for (const file of files) {
// Only process XML files // Only process XML files
if (!file.toLowerCase().endsWith(".xml")) { 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 filePath = path.join(config.path!, file);
const stats = await fs.stat(filePath); try {
if (!stats.isFile()) { const stats = await fs.stat(filePath);
if (!stats.isFile()) {
continue;
}
} catch (statError) {
log.warn(`Failed to stat file ${filePath}:`, statError);
continue; continue;
} }
@@ -46,40 +70,61 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
} }
// Validate XML structure // Validate XML structure
let xmlContent : BlobPart; let xmlContent: BlobPart;
try { try {
xmlContent = await fs.readFile(filePath, "utf8"); xmlContent = await fs.readFile(filePath, "utf8");
await parseStringPromise(xmlContent); await parseStringPromise(xmlContent);
log.debug(`Successfully validated XML for ${filePath}`);
} catch (error) { } catch (error) {
log.error(`Invalid XML in ${filePath}:`, error); log.error(`Invalid XML in ${filePath}:`, error);
const timestamp = Date.now().toString(); // similar to DateTime.Now.Ticks in C# const timestamp = dayjs().format("YYYYMMDD_HHmmss");
const errorPath = path.join(errorDir, `${timestamp}.xml`); const originalFilename = path.basename(file, path.extname(file));
await fs.rename(filePath, errorPath); const errorPath = path.join(
log.debug(`Moved invalid file to error: ${errorPath}`); 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; continue;
} }
// Get authentication token // Get authentication token
const token = (store.get("user") as any)?.stsTokenManager?.accessToken; let token: string | null;
if (!token) { try {
log.error(`No authentication token for file: ${filePath}`); 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; continue;
} }
// Upload file to API // Upload file to API
const formData = new FormData(); const formData = new FormData();
formData.append("file", new Blob([xmlContent]), path.basename(filePath)); formData.append("file", new Blob([xmlContent]), path.basename(filePath));
formData.append( const shopId = (store.get("app.bodyshop") as any)?.shopname || "";
"shopId", formData.append("shopId", shopId);
(store.get("app.bodyshop") as any)?.shopname || "", log.debug(`Shop ID: ${shopId}`);
);
const baseURL = store.get("app.isTest") const baseURL = store.get("app.isTest")
? import.meta.env.VITE_API_TEST_URL ? import.meta.env.VITE_API_TEST_URL
: import.meta.env.VITE_API_URL; : import.meta.env.VITE_API_URL;
const finalUrl = `${baseURL}/mixdata/upload`; const finalUrl = `${baseURL}/mixdata/upload`;
log.debug(`Uploading file to ${finalUrl}`); log.debug(`Uploading file to ${finalUrl}`);
try { try {
@@ -88,23 +133,52 @@ export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data", "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) { if (response.status === 200) {
log.info(`Successful upload of ${filePath}`); log.info(`Successful upload of ${filePath}`);
// Move file to archive // Move file to archive
const timestamp = Date.now().toString(); // generate new timestamp const timestamp = dayjs().format("YYYYMMDD_HHmmss");
const archivePath = path.join(archiveDir, `${timestamp}.xml`); const originalFilename = path.basename(file, path.extname(file));
await fs.rename(filePath, archivePath); const archivePath = path.join(
log.debug(`Moved file to archive: ${archivePath}`); 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 { } else {
log.error( log.error(
`Failed to upload ${filePath}: ${response.status} ${response.statusText}`, `Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
response.data, { responseData: response.data },
); );
} }
} catch (error) { } catch (error: any) {
log.error(`Error uploading ${filePath}:`, error); 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) { } catch (error) {
@@ -121,70 +195,16 @@ export async function ppgOutputHandler(
await fs.mkdir(config.path!, { recursive: true }); await fs.mkdir(config.path!, { recursive: true });
const query = ` const variables: PpgDataQueryVariables = {
query PpgData($today: timestamptz!, $todayplus5: timestamptz!, $shopid: uuid!) { today: dayjs().toISOString(),
bodyshops_by_pk(id:$shopid) { todayplus5: dayjs().add(5, "day").toISOString(),
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, 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 jobs = response.jobs ?? [];
const header = { const header = {
@@ -197,18 +217,10 @@ export async function ppgOutputHandler(
}, },
Transaction: { Transaction: {
TransactionID: "", TransactionID: "",
TransactionDate: (() => { TransactionDate: dayjs().format("YYYY-MM-DD:HH:mm"),
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: { Product: {
Name: "ImEX Online", Name: import.meta.env.VITE_COMPANY === "IMEX",
Version: "", Version: "",
}, },
}, },
@@ -220,7 +232,7 @@ export async function ppgOutputHandler(
}, },
RepairOrders: { RepairOrders: {
ROCount: jobs.length.toString(), ROCount: jobs.length.toString(),
RO: jobs.map((job: any) => ({ RO: jobs.map((job) => ({
RONumber: job.ro_number || "", RONumber: job.ro_number || "",
ROStatus: "Open", ROStatus: "Open",
Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`, 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); log.error(`Error generating PPG output for config ${config.id}:`, error);
} }
} }

View File

@@ -19,7 +19,7 @@ export async function setupKeepAliveAgent(): Promise<void> {
<string>com.convenientbrands.bodyshop-desktop.keepalive</string> <string>com.convenientbrands.bodyshop-desktop.keepalive</string>
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>open</string> <string>Shop Partner Keep Alive</string>
<string>imexmedia://keep-alive</string> <string>imexmedia://keep-alive</string>
</array> </array>
<key>RunAtLoad</key> <key>RunAtLoad</key>
@@ -30,8 +30,8 @@ export async function setupKeepAliveAgent(): Promise<void> {
</plist>`; </plist>`;
const plistPath = join( const plistPath = join(
homedir(), homedir(),
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist", "/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
); );
try { try {
@@ -40,15 +40,17 @@ export async function setupKeepAliveAgent(): Promise<void> {
log.info(`Launch agent created and loaded: ${stdout}`); log.info(`Launch agent created and loaded: ${stdout}`);
if (stderr) log.warn(`Launch agent stderr: ${stderr}`); if (stderr) log.warn(`Launch agent stderr: ${stderr}`);
} catch (error) { } 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 throw error; // Rethrow to allow caller to handle
} }
} }
export async function isKeepAliveAgentInstalled(): Promise<boolean> { export async function isKeepAliveAgentInstalled(): Promise<boolean> {
const plistPath = join( const plistPath = join(
homedir(), homedir(),
"Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist", "/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
); );
const maxRetries = 3; const maxRetries = 3;
const retryDelay = 500; // 500ms delay between retries const retryDelay = 500; // 500ms delay between retries
@@ -56,10 +58,14 @@ export async function isKeepAliveAgentInstalled(): Promise<boolean> {
for (let attempt = 1; attempt <= maxRetries; attempt++) { for (let attempt = 1; attempt <= maxRetries; attempt++) {
try { try {
await fs.access(plistPath, fs.constants.F_OK); 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 return !!stdout; // Return true if plist exists and agent is loaded
} catch (error) { } 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) { if (attempt === maxRetries) {
return false; // Return false after all retries fail return false; // Return false after all retries fail
} }
@@ -68,4 +74,4 @@ export async function isKeepAliveAgentInstalled(): Promise<boolean> {
} }
} }
return false; // Fallback return return false; // Fallback return
} }

View File

@@ -6,9 +6,9 @@ const execPromise = promisify(exec);
// Define the interval as a variable (in minutes) // Define the interval as a variable (in minutes)
const KEEP_ALIVE_INTERVAL_MINUTES = 15; const KEEP_ALIVE_INTERVAL_MINUTES = 15;
const taskName = "ShopPartnerKeepAlive";
export async function setupKeepAliveTask(): Promise<void> { export async function setupKeepAliveTask(): Promise<void> {
const taskName = "ImEXShopPartnerKeepAlive";
const protocolUrl = "imexmedia://keep-alive"; const protocolUrl = "imexmedia://keep-alive";
// Use rundll32.exe to silently open the URL as a protocol // Use rundll32.exe to silently open the URL as a protocol
const command = `rundll32.exe url.dll,OpenURL "${protocolUrl}"`; const command = `rundll32.exe url.dll,OpenURL "${protocolUrl}"`;
@@ -30,7 +30,6 @@ export async function setupKeepAliveTask(): Promise<void> {
} }
export async function isKeepAliveTaskInstalled(): Promise<boolean> { export async function isKeepAliveTaskInstalled(): Promise<boolean> {
const taskName = "ImEXShopPartnerKeepAlive";
const maxRetries = 3; const maxRetries = 3;
const retryDelay = 500; // 500ms delay between retries const retryDelay = 500; // 500ms delay between retries

View File

@@ -56,7 +56,7 @@ export const ttlFieldLineDescriptors: FieldDescriptor[] = [
decimalPlaces: 2, decimalPlaces: 2,
}, },
{ {
name: "N_SUPP_ANT", name: "N_SUPP_AMT",
type: "N", type: "N",
size: 10, size: 10,
decimalPlaces: 2, decimalPlaces: 2,

View 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;

View File

@@ -13,9 +13,10 @@ import {
Space, Space,
Table, Table,
Tag, Tag,
theme,
Tooltip, Tooltip,
} from "antd"; } from "antd";
import { FC, useState } from "react"; import { JSX, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
PaintScaleConfig, PaintScaleConfig,
@@ -24,8 +25,10 @@ import {
} from "../../../../util/types/paintScale"; } from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig"; import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleInputPaths: FC = () => { const SettingsPaintScaleInputPaths = (): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const { token } = theme.useToken(); // Access theme tokens
const { const {
paintScaleConfigs, paintScaleConfigs,
handleAddConfig, handleAddConfig,
@@ -87,7 +90,7 @@ const SettingsPaintScaleInputPaths: FC = () => {
placeholder={t("settings.labels.paintScalePath")} placeholder={t("settings.labels.paintScalePath")}
disabled disabled
style={{ style={{
borderColor: isValid ? "#52c41a" : "#d9d9d9", borderColor: isValid ? token.colorSuccess : token.colorError, // Use semantic tokens
}} }}
suffix={ suffix={
<Tooltip <Tooltip
@@ -98,9 +101,9 @@ const SettingsPaintScaleInputPaths: FC = () => {
} }
> >
{isValid ? ( {isValid ? (
<CheckCircleFilled style={{ color: "#52c41a" }} /> <CheckCircleFilled style={{ color: token.colorSuccess }} />
) : ( ) : (
<WarningFilled style={{ color: "#faad14" }} /> <WarningFilled style={{ color: token.colorError }} />
)} )}
</Tooltip> </Tooltip>
} }

View File

@@ -4,8 +4,18 @@ import {
FolderOpenFilled, FolderOpenFilled,
WarningFilled, WarningFilled,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Button, Card, Input, Modal, Select, Space, Table, Tag } from "antd"; import {
import { FC, useState } from "react"; Button,
Card,
Input,
Modal,
Select,
Space,
Table,
Tag,
theme,
} from "antd";
import { JSX, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
PaintScaleConfig, PaintScaleConfig,
@@ -14,7 +24,8 @@ import {
} from "../../../../util/types/paintScale"; } from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig"; import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleOutputPaths: FC = () => { const SettingsPaintScaleOutputPaths = (): JSX.Element => {
const { token } = theme.useToken();
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
paintScaleConfigs, paintScaleConfigs,
@@ -77,13 +88,13 @@ const SettingsPaintScaleOutputPaths: FC = () => {
placeholder={t("settings.labels.paintScalePath")} placeholder={t("settings.labels.paintScalePath")}
disabled disabled
style={{ style={{
borderColor: isValid ? "#52c41a" : "#d9d9d9", borderColor: isValid ? token.colorSuccess : token.colorError,
}} }}
suffix={ suffix={
isValid ? ( isValid ? (
<CheckCircleFilled style={{ color: "#52c41a" }} /> <CheckCircleFilled style={{ color: token.colorSuccess }} />
) : ( ) : (
<WarningFilled style={{ color: "#faad14" }} /> <WarningFilled style={{ color: token.colorError }} />
) )
} }
/> />