Initial copy of shop partner app.
This commit is contained in:
278
src/main/ipc/ipcMainConfig.ts
Normal file
278
src/main/ipc/ipcMainConfig.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { app, ipcMain } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import path from "path";
|
||||
import ipcTypes from "../../util/ipcTypes.json";
|
||||
import ImportJob from "../decoder/decoder";
|
||||
import store from "../store/store";
|
||||
import { StartWatcher, StopWatcher } from "../watcher/watcher";
|
||||
import {
|
||||
SettingEmsOutFilePathGet,
|
||||
SettingEmsOutFilePathSet,
|
||||
SettingsPaintScaleInputConfigsGet,
|
||||
SettingsPaintScaleInputConfigsSet,
|
||||
SettingsPaintScaleInputPathSet,
|
||||
SettingsPaintScaleOutputConfigsGet,
|
||||
SettingsPaintScaleOutputConfigsSet,
|
||||
SettingsPaintScaleOutputPathSet,
|
||||
SettingsPpcFilePathGet,
|
||||
SettingsPpcFilePathSet,
|
||||
SettingsWatchedFilePathsAdd,
|
||||
SettingsWatchedFilePathsGet,
|
||||
SettingsWatchedFilePathsRemove,
|
||||
SettingsWatcherPollingGet,
|
||||
SettingsWatcherPollingSet,
|
||||
} from "./ipcMainHandler.settings";
|
||||
import {
|
||||
ipcMainHandleAuthStateChanged,
|
||||
ipMainHandleResetPassword,
|
||||
} from "./ipcMainHandler.user";
|
||||
import cron from "node-cron";
|
||||
import { PaintScaleConfig, PaintScaleType } from "../../util/types/paintScale";
|
||||
import { ppgInputHandler, ppgOutputHandler } from "./paintScaleHandlers/PPG";
|
||||
|
||||
const initializeCronTasks = async () => {
|
||||
try {
|
||||
// Fetch input and output configurations
|
||||
const inputConfigs = await SettingsPaintScaleInputConfigsGet();
|
||||
const outputConfigs = await SettingsPaintScaleOutputConfigsGet();
|
||||
|
||||
// Start input cron tasks
|
||||
await handlePaintScaleInputCron(inputConfigs);
|
||||
log.info("Initialized input cron tasks on app startup");
|
||||
|
||||
// Start output cron tasks
|
||||
await handlePaintScaleOutputCron(outputConfigs);
|
||||
log.info("Initialized output cron tasks on app startup");
|
||||
} catch (error) {
|
||||
log.error("Error initializing cron tasks on app startup:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Log all IPC messages and their payloads
|
||||
const logIpcMessages = (): void => {
|
||||
Object.keys(ipcTypes.toMain).forEach((key) => {
|
||||
const messageType = ipcTypes.toMain[key];
|
||||
const originalHandler = ipcMain.listeners(messageType)?.[0];
|
||||
if (originalHandler) {
|
||||
ipcMain.removeAllListeners(messageType);
|
||||
}
|
||||
ipcMain.on(messageType, (event, payload) => {
|
||||
log.info(
|
||||
`%c[IPC Main]%c${messageType}`,
|
||||
"color: red",
|
||||
"color: green",
|
||||
payload,
|
||||
);
|
||||
if (originalHandler) {
|
||||
originalHandler(event, payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Input handler map
|
||||
const inputTypeHandlers: Partial<
|
||||
Record<PaintScaleType, (config: PaintScaleConfig) => Promise<void>>
|
||||
> = {
|
||||
[PaintScaleType.PPG]: ppgInputHandler,
|
||||
// Add other input type handlers as needed
|
||||
};
|
||||
|
||||
// Output handler map
|
||||
const outputTypeHandlers: Partial<
|
||||
Record<PaintScaleType, (config: PaintScaleConfig) => Promise<void>>
|
||||
> = {
|
||||
[PaintScaleType.PPG]: ppgOutputHandler,
|
||||
// Add other output type handlers as needed
|
||||
};
|
||||
|
||||
// Default handler for unsupported types
|
||||
const defaultHandler = async (config: PaintScaleConfig) => {
|
||||
log.debug(
|
||||
`No handler defined for type ${config.type} in config ${config.id}`,
|
||||
);
|
||||
};
|
||||
|
||||
// Input cron job management
|
||||
let inputCronTasks: { [id: string]: cron.ScheduledTask } = {};
|
||||
|
||||
const handlePaintScaleInputCron = async (configs: PaintScaleConfig[]) => {
|
||||
Object.values(inputCronTasks).forEach((task) => task.stop());
|
||||
inputCronTasks = {};
|
||||
|
||||
const validConfigs = configs.filter(
|
||||
(config) => config.path && config.path.trim() !== "",
|
||||
);
|
||||
|
||||
validConfigs.forEach((config) => {
|
||||
const cronExpression = `*/${config.pollingInterval} * * * *`;
|
||||
inputCronTasks[config.id] = cron.schedule(cronExpression, async () => {
|
||||
const handler = inputTypeHandlers[config.type] || defaultHandler;
|
||||
await handler(config);
|
||||
});
|
||||
log.info(
|
||||
`Started input cron task for config ${config.id} (type: ${config.type}) with interval ${config.pollingInterval}m`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Output cron job management
|
||||
let outputCronTasks: { [id: string]: cron.ScheduledTask } = {};
|
||||
|
||||
const handlePaintScaleOutputCron = async (configs: PaintScaleConfig[]) => {
|
||||
Object.values(outputCronTasks).forEach((task) => task.stop());
|
||||
outputCronTasks = {};
|
||||
|
||||
const validConfigs = configs.filter(
|
||||
(config) => config.path && config.path.trim() !== "",
|
||||
);
|
||||
|
||||
validConfigs.forEach((config) => {
|
||||
const cronExpression = `*/${config.pollingInterval} * * * *`;
|
||||
outputCronTasks[config.id] = cron.schedule(cronExpression, async () => {
|
||||
const handler = outputTypeHandlers[config.type] || defaultHandler;
|
||||
await handler(config);
|
||||
});
|
||||
log.info(
|
||||
`Started output cron task for config ${config.id} (type: ${config.type}) with interval ${config.pollingInterval}m`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Existing IPC handlers...
|
||||
|
||||
ipcMain.on(ipcTypes.toMain.test, () =>
|
||||
console.log("** Verify that ipcMain is loaded and working."),
|
||||
);
|
||||
|
||||
// Auth handler
|
||||
ipcMain.on(ipcTypes.toMain.authStateChanged, ipcMainHandleAuthStateChanged);
|
||||
ipcMain.on(ipcTypes.toMain.user.resetPassword, ipMainHandleResetPassword);
|
||||
|
||||
// Add debug handlers if in development
|
||||
if (import.meta.env.DEV) {
|
||||
log.debug("[IPC Debug Functions] Adding Debug Handlers");
|
||||
|
||||
ipcMain.on(ipcTypes.toMain.debug.decodeEstimate, async (): Promise<void> => {
|
||||
const relativeEmsFilepath = `_reference/ems/MPI_1/3698420.ENV`;
|
||||
const rootDir = app.getAppPath();
|
||||
const absoluteFilepath = path.join(rootDir, relativeEmsFilepath);
|
||||
|
||||
log.debug("[IPC Debug Function] Decode test Estimate", absoluteFilepath);
|
||||
await ImportJob(absoluteFilepath);
|
||||
|
||||
const job2 = `/Users/pfic/Downloads/12285264/2285264.ENV`;
|
||||
const job3 = `/Users/pfic/Downloads/14033376/4033376.ENV`;
|
||||
await ImportJob(job2);
|
||||
await ImportJob(job3);
|
||||
});
|
||||
}
|
||||
|
||||
// Settings Handlers
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.filepaths.get,
|
||||
SettingsWatchedFilePathsGet,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.filepaths.add,
|
||||
SettingsWatchedFilePathsAdd,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.filepaths.remove,
|
||||
SettingsWatchedFilePathsRemove,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.watcher.getpolling,
|
||||
SettingsWatcherPollingGet,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.watcher.setpolling,
|
||||
SettingsWatcherPollingSet,
|
||||
);
|
||||
|
||||
ipcMain.handle(ipcTypes.toMain.settings.getPpcFilePath, SettingsPpcFilePathGet);
|
||||
ipcMain.handle(ipcTypes.toMain.settings.setPpcFilePath, SettingsPpcFilePathSet);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.getEmsOutFilePath,
|
||||
SettingEmsOutFilePathGet,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.setEmsOutFilePath,
|
||||
SettingEmsOutFilePathSet,
|
||||
);
|
||||
|
||||
// Paint Scale Input Settings Handlers
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.paintScale.getInputConfigs,
|
||||
SettingsPaintScaleInputConfigsGet,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.paintScale.setInputConfigs,
|
||||
SettingsPaintScaleInputConfigsSet,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.paintScale.setInputPath,
|
||||
SettingsPaintScaleInputPathSet,
|
||||
);
|
||||
|
||||
// Paint Scale Output Settings Handlers
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
|
||||
SettingsPaintScaleOutputConfigsGet,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.paintScale.setOutputConfigs,
|
||||
SettingsPaintScaleOutputConfigsSet,
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcTypes.toMain.settings.paintScale.setOutputPath,
|
||||
SettingsPaintScaleOutputPathSet,
|
||||
);
|
||||
|
||||
// IPC handlers for updating paint scale cron
|
||||
ipcMain.on(
|
||||
ipcTypes.toMain.settings.paintScale.updateInputCron,
|
||||
(_event, configs: PaintScaleConfig[]) => {
|
||||
handlePaintScaleInputCron(configs).catch((error) => {
|
||||
log.error(`Error handling paint scale input cron for configs: ${error}`);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on(
|
||||
ipcTypes.toMain.settings.paintScale.updateOutputCron,
|
||||
(_event, configs: PaintScaleConfig[]) => {
|
||||
handlePaintScaleOutputCron(configs).catch((error) => {
|
||||
log.error(`Error handling paint scale output cron for configs: ${error}`);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(ipcTypes.toMain.user.getActiveShop, () => {
|
||||
return store.get("app.bodyshop.shopname");
|
||||
});
|
||||
|
||||
// Watcher Handlers
|
||||
ipcMain.on(ipcTypes.toMain.watcher.start, () => {
|
||||
StartWatcher().catch((error) => {
|
||||
log.error("Error starting watcher:", error);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(ipcTypes.toMain.watcher.stop, () => {
|
||||
StopWatcher().catch((error) => {
|
||||
log.error("Error stopping watcher:", error);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(ipcTypes.toMain.updates.download, () => {
|
||||
log.info("Download update requested from renderer.");
|
||||
autoUpdater.downloadUpdate().catch((error) => {
|
||||
log.error("Error downloading update:", error);
|
||||
});
|
||||
});
|
||||
|
||||
export { initializeCronTasks };
|
||||
|
||||
logIpcMessages();
|
||||
67
src/main/ipc/ipcMainConfig.types.ts
Normal file
67
src/main/ipc/ipcMainConfig.types.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export interface User {
|
||||
stsTokenManager?: {
|
||||
accessToken: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BodyShop {
|
||||
shopname: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface GraphQLResponse {
|
||||
bodyshops_by_pk?: {
|
||||
imexshopid: string;
|
||||
shopname: string;
|
||||
};
|
||||
jobs?: Array<{
|
||||
labhrs: any;
|
||||
larhrs: any;
|
||||
ro_number: string;
|
||||
ownr_ln: string;
|
||||
ownr_fn: string;
|
||||
plate_no: string;
|
||||
v_vin: string;
|
||||
v_model_yr: string;
|
||||
v_make_desc: string;
|
||||
v_model_desc: string;
|
||||
vehicle?: {
|
||||
v_paint_codes?: {
|
||||
paint_cd1: string;
|
||||
};
|
||||
};
|
||||
larhrs_aggregate?: {
|
||||
aggregate?: {
|
||||
sum?: {
|
||||
mod_lb_hrs: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
ins_co_nm: string;
|
||||
est_ct_ln: string;
|
||||
est_ct_fn: string;
|
||||
job_totals?: {
|
||||
rates?: {
|
||||
mapa?: {
|
||||
total?: {
|
||||
amount: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
totals?: {
|
||||
subtotal?: {
|
||||
amount: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
rate_mapa: number;
|
||||
labhrs_aggregate?: {
|
||||
aggregate?: {
|
||||
sum?: {
|
||||
mod_lb_hrs: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
rate_lab: number;
|
||||
}>;
|
||||
}
|
||||
257
src/main/ipc/ipcMainHandler.settings.ts
Normal file
257
src/main/ipc/ipcMainHandler.settings.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
// main/ipcMainHandler.settings.ts
|
||||
import { dialog, IpcMainInvokeEvent } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import _ from "lodash";
|
||||
import Store from "../store/store";
|
||||
import { getMainWindow } from "../util/toRenderer";
|
||||
import {
|
||||
addWatcherPath,
|
||||
removeWatcherPath,
|
||||
StartWatcher,
|
||||
StopWatcher,
|
||||
} from "../watcher/watcher";
|
||||
import { PaintScaleConfig } from "../../util/types/paintScale";
|
||||
|
||||
// Initialize paint scale input configs in store if not set
|
||||
if (!Store.get("settings.paintScaleInputConfigs")) {
|
||||
Store.set("settings.paintScaleInputConfigs", []);
|
||||
}
|
||||
|
||||
// Initialize paint scale output configs in store if not set
|
||||
if (!Store.get("settings.paintScaleOutputConfigs")) {
|
||||
Store.set("settings.paintScaleOutputConfigs", []);
|
||||
}
|
||||
|
||||
const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
log.error("No main window found when trying to open dialog");
|
||||
return [];
|
||||
}
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
Store.set(
|
||||
"settings.filepaths",
|
||||
_.union(result.filePaths, Store.get("settings.filepaths")),
|
||||
);
|
||||
addWatcherPath(result.filePaths);
|
||||
}
|
||||
|
||||
return Store.get("settings.filepaths");
|
||||
};
|
||||
|
||||
const SettingsWatchedFilePathsRemove = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
path: string,
|
||||
): Promise<string[]> => {
|
||||
Store.set(
|
||||
"settings.filepaths",
|
||||
_.without(Store.get("settings.filepaths"), path),
|
||||
);
|
||||
removeWatcherPath(path);
|
||||
return Store.get("settings.filepaths");
|
||||
};
|
||||
|
||||
const SettingsWatchedFilePathsGet = async (): Promise<string[]> => {
|
||||
return Store.get("settings.filepaths") || [];
|
||||
};
|
||||
|
||||
const SettingsWatcherPollingGet = async (): Promise<{
|
||||
enabled: boolean;
|
||||
interval: number;
|
||||
}> => {
|
||||
const pollingEnabled: { enabled: boolean; interval: number } =
|
||||
Store.get("settings.polling");
|
||||
return { enabled: pollingEnabled.enabled, interval: pollingEnabled.interval };
|
||||
};
|
||||
|
||||
const SettingsWatcherPollingSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
pollingSettings: {
|
||||
enabled: boolean;
|
||||
interval: number;
|
||||
},
|
||||
): Promise<{
|
||||
enabled: boolean;
|
||||
interval: number;
|
||||
}> => {
|
||||
log.info("Polling set", pollingSettings);
|
||||
const { enabled, interval } = pollingSettings;
|
||||
Store.set("settings.polling", { enabled, interval });
|
||||
|
||||
await StopWatcher();
|
||||
await StartWatcher();
|
||||
|
||||
return { enabled, interval };
|
||||
};
|
||||
|
||||
const SettingsPpcFilePathGet = async (): Promise<string> => {
|
||||
return Store.get("settings.ppcFilePath");
|
||||
};
|
||||
|
||||
const SettingsPpcFilePathSet = async (): Promise<string> => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
log.error("No main window found when trying to open dialog");
|
||||
return "";
|
||||
}
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
Store.set("settings.ppcFilePath", result.filePaths[0]);
|
||||
}
|
||||
|
||||
return (Store.get("settings.ppcFilePath") as string) || "";
|
||||
};
|
||||
|
||||
const SettingEmsOutFilePathGet = async (): Promise<string> => {
|
||||
return Store.get("settings.emsOutFilePath");
|
||||
};
|
||||
|
||||
const SettingEmsOutFilePathSet = async (): Promise<string> => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
log.error("No main window found when trying to open dialog");
|
||||
return "";
|
||||
}
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
|
||||
if (!result.canceled) {
|
||||
Store.set("settings.emsOutFilePath", result.filePaths[0]);
|
||||
}
|
||||
|
||||
return (Store.get("settings.emsOutFilePath") as string) || "";
|
||||
};
|
||||
|
||||
const SettingsPaintScaleInputConfigsGet = (
|
||||
_event?: IpcMainInvokeEvent,
|
||||
): PaintScaleConfig[] => {
|
||||
try {
|
||||
const configs = Store.get(
|
||||
"settings.paintScaleInputConfigs",
|
||||
) as PaintScaleConfig[];
|
||||
log.debug("Retrieved paint scale input configs:", configs);
|
||||
return configs || [];
|
||||
} catch (error) {
|
||||
log.error("Error getting paint scale input configs:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsPaintScaleInputConfigsSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
configs: PaintScaleConfig[],
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
Store.set("settings.paintScaleInputConfigs", configs);
|
||||
log.debug("Saved paint scale input configs:", configs);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("Error setting paint scale input configs:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsPaintScaleInputPathSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
log.error("No main window found when trying to open dialog");
|
||||
return null;
|
||||
}
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
if (result.canceled) {
|
||||
log.debug("Paint scale input path selection canceled");
|
||||
return null;
|
||||
}
|
||||
const path = result.filePaths[0];
|
||||
log.debug("Selected paint scale input path:", path);
|
||||
return path;
|
||||
} catch (error) {
|
||||
log.error("Error setting paint scale input path:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsPaintScaleOutputConfigsGet = (
|
||||
_event?: IpcMainInvokeEvent,
|
||||
): PaintScaleConfig[] => {
|
||||
try {
|
||||
const configs = Store.get(
|
||||
"settings.paintScaleOutputConfigs",
|
||||
) as PaintScaleConfig[];
|
||||
log.debug("Retrieved paint scale output configs:", configs);
|
||||
return configs || [];
|
||||
} catch (error) {
|
||||
log.error("Error getting paint scale output configs:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsPaintScaleOutputConfigsSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
configs: PaintScaleConfig[],
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
Store.set("settings.paintScaleOutputConfigs", configs);
|
||||
log.debug("Saved paint scale output configs:", configs);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("Error setting paint scale output configs:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const SettingsPaintScaleOutputPathSet = async (
|
||||
_event: IpcMainInvokeEvent,
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
log.error("No main window found when trying to open dialog");
|
||||
return null;
|
||||
}
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
properties: ["openDirectory"],
|
||||
});
|
||||
if (result.canceled) {
|
||||
log.debug("Paint scale output path selection canceled");
|
||||
return null;
|
||||
}
|
||||
const path = result.filePaths[0];
|
||||
log.debug("Selected paint scale output path:", path);
|
||||
return path;
|
||||
} catch (error) {
|
||||
log.error("Error setting paint scale output path:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
SettingsPpcFilePathGet,
|
||||
SettingsPpcFilePathSet,
|
||||
SettingsWatchedFilePathsAdd,
|
||||
SettingsWatchedFilePathsGet,
|
||||
SettingsWatchedFilePathsRemove,
|
||||
SettingsWatcherPollingGet,
|
||||
SettingsWatcherPollingSet,
|
||||
SettingEmsOutFilePathGet,
|
||||
SettingEmsOutFilePathSet,
|
||||
SettingsPaintScaleInputConfigsGet,
|
||||
SettingsPaintScaleInputConfigsSet,
|
||||
SettingsPaintScaleInputPathSet,
|
||||
SettingsPaintScaleOutputConfigsGet,
|
||||
SettingsPaintScaleOutputConfigsSet,
|
||||
SettingsPaintScaleOutputPathSet,
|
||||
};
|
||||
102
src/main/ipc/ipcMainHandler.user.ts
Normal file
102
src/main/ipc/ipcMainHandler.user.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { IpcMainEvent, shell } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { User } from "firebase/auth";
|
||||
import errorTypeCheck from "../../util/errorTypeCheck";
|
||||
import ipcTypes from "../../util/ipcTypes.json";
|
||||
import client from "../graphql/graphql-client";
|
||||
import {
|
||||
ActiveBodyshopQueryResult,
|
||||
MasterdataQueryResult,
|
||||
QUERY_ACTIVE_BODYSHOP_TYPED,
|
||||
QUERY_MASTERDATA_TYPED,
|
||||
} from "../graphql/queries";
|
||||
import { default as Store, default as store } from "../store/store";
|
||||
import { checkForAppUpdatesContinuously } from "../util/checkForAppUpdates";
|
||||
import { getMainWindow, sendIpcToRenderer } from "../util/toRenderer";
|
||||
|
||||
const ipcMainHandleAuthStateChanged = async (
|
||||
_event: IpcMainEvent,
|
||||
user: User | null,
|
||||
): Promise<void> => {
|
||||
Store.set("user", user);
|
||||
log.debug("Received authentication state change from Renderer.", user);
|
||||
await setReleaseChannel();
|
||||
checkForAppUpdatesContinuously();
|
||||
};
|
||||
|
||||
async function setReleaseChannel() {
|
||||
try {
|
||||
//Need to query the currently active shop, and store the metadata as well.
|
||||
//Also need to query the OP Codes for decoding reference.
|
||||
await handleShopMetaDataFetch();
|
||||
//Check for updates
|
||||
const bodyshop = Store.get("app.bodyshop");
|
||||
if (bodyshop?.convenient_company?.toLowerCase() === "alpha") {
|
||||
autoUpdater.channel = "alpha";
|
||||
log.debug("Setting update channel to ALPHA channel.");
|
||||
} else if (bodyshop?.convenient_company?.toLowerCase() === "beta") {
|
||||
autoUpdater.channel = "beta";
|
||||
log.debug("Setting update channel to BETA channel.");
|
||||
} else {
|
||||
log.debug("Setting update channel to LATEST channel.");
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(
|
||||
"Error while querying active bodyshop or master data",
|
||||
errorTypeCheck(error),
|
||||
);
|
||||
sendIpcToRenderer(
|
||||
ipcTypes.toRenderer.general.showErrorMessage,
|
||||
"Error connecting to ImEX Online servers to get shop data. Please try again.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const handleShopMetaDataFetch = async (
|
||||
reloadWindow?: boolean,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
log.debug("Requery shop information & master data.");
|
||||
const activeBodyshop: ActiveBodyshopQueryResult = await client.request(
|
||||
QUERY_ACTIVE_BODYSHOP_TYPED,
|
||||
);
|
||||
|
||||
Store.set("app.bodyshop", activeBodyshop.bodyshops[0]);
|
||||
|
||||
const OpCodes: MasterdataQueryResult = await client.request(
|
||||
QUERY_MASTERDATA_TYPED,
|
||||
{
|
||||
key: `${activeBodyshop.bodyshops[0].region_config}_ciecaopcodes`,
|
||||
},
|
||||
);
|
||||
Store.set(
|
||||
"app.masterdata.opcodes",
|
||||
JSON.parse(OpCodes.masterdata[0]?.value),
|
||||
);
|
||||
if (reloadWindow) {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.reload();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Error while fetching shop metadata", errorTypeCheck(error));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const ipMainHandleResetPassword = async (): Promise<void> => {
|
||||
shell.openExternal(
|
||||
store.get("app.isTest")
|
||||
? `${import.meta.env.VITE_FE_URL_TEST}/resetpassword`
|
||||
: `${import.meta.env.VITE_FE_URL}/resetpassword`,
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
handleShopMetaDataFetch,
|
||||
ipcMainHandleAuthStateChanged,
|
||||
ipMainHandleResetPassword,
|
||||
setReleaseChannel,
|
||||
};
|
||||
272
src/main/ipc/paintScaleHandlers/PPG.ts
Normal file
272
src/main/ipc/paintScaleHandlers/PPG.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import log from "electron-log/main";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import axios from "axios";
|
||||
import { create } from "xmlbuilder2";
|
||||
import { parseStringPromise } from "xml2js";
|
||||
import store from "../../store/store";
|
||||
import client, { 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";
|
||||
|
||||
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");
|
||||
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")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(config.path!, file);
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
if (!stats.isFile()) {
|
||||
continue;
|
||||
}
|
||||
} catch (statError) {
|
||||
log.warn(`Failed to stat file ${filePath}:`, statError);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debug(`Processing input file: ${filePath}`);
|
||||
|
||||
// Check file accessibility (e.g., not locked)
|
||||
try {
|
||||
await fs.access(filePath, fs.constants.R_OK);
|
||||
} catch (error) {
|
||||
log.warn(`File ${filePath} is inaccessible, skipping:`, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate XML structure
|
||||
let xmlContent: BlobPart;
|
||||
try {
|
||||
xmlContent = await fs.readFile(filePath, "utf8");
|
||||
await parseStringPromise(xmlContent);
|
||||
log.debug(`Successfully validated XML for ${filePath}`);
|
||||
} catch (error) {
|
||||
log.error(`Invalid XML in ${filePath}:`, error);
|
||||
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
|
||||
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));
|
||||
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 {
|
||||
const response = await axios.post(finalUrl, formData, {
|
||||
headers: {
|
||||
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 = 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}`,
|
||||
{ responseData: response.data },
|
||||
);
|
||||
}
|
||||
} 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) {
|
||||
log.error(`Error polling input directory ${config.path}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// PPG Output Handler
|
||||
export async function ppgOutputHandler(
|
||||
config: PaintScaleConfig,
|
||||
): Promise<void> {
|
||||
try {
|
||||
log.info(`Generating PPG output for config ${config.id}: ${config.path}`);
|
||||
|
||||
await fs.mkdir(config.path!, { recursive: true });
|
||||
|
||||
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<
|
||||
PpgDataQueryResult,
|
||||
PpgDataQueryVariables
|
||||
>(PPG_DATA_QUERY_TYPED, variables);
|
||||
const jobs = response.jobs ?? [];
|
||||
|
||||
const header = {
|
||||
PPG: {
|
||||
Header: {
|
||||
Protocol: {
|
||||
Message: "PaintShopInterface",
|
||||
Name: "PPG",
|
||||
Version: "1.5.0",
|
||||
},
|
||||
Transaction: {
|
||||
TransactionID: "",
|
||||
TransactionDate: dayjs().format("YYYY-MM-DD:HH:mm"),
|
||||
},
|
||||
Product: {
|
||||
Name: import.meta.env.VITE_COMPANY === "IMEX",
|
||||
Version: "",
|
||||
},
|
||||
},
|
||||
DataInterface: {
|
||||
ROData: {
|
||||
ShopInfo: {
|
||||
ShopID: response.bodyshops_by_pk?.imexshopid || "",
|
||||
ShopName: response.bodyshops_by_pk?.shopname || "",
|
||||
},
|
||||
RepairOrders: {
|
||||
ROCount: jobs.length.toString(),
|
||||
RO: jobs.map((job) => ({
|
||||
RONumber: job.ro_number || "",
|
||||
ROStatus: "Open",
|
||||
Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`,
|
||||
ROPainterNotes: "",
|
||||
LicensePlateNum: job.plate_no || "",
|
||||
VIN: job.v_vin || "",
|
||||
ModelYear: job.v_model_yr || "",
|
||||
MakeDesc: job.v_make_desc || "",
|
||||
ModelName: job.v_model_desc || "",
|
||||
OEMColorCode: job.vehicle?.v_paint_codes?.paint_cd1 || "",
|
||||
RefinishLaborHours: job.larhrs?.aggregate?.sum?.mod_lb_hrs || 0,
|
||||
InsuranceCompanyName: job.ins_co_nm || "",
|
||||
EstimatorName: `${job.est_ct_ln || ""}, ${job.est_ct_fn || ""}`,
|
||||
PaintMaterialsRevenue: (
|
||||
(job.job_totals?.rates?.mapa?.total?.amount || 0) / 100
|
||||
).toFixed(2),
|
||||
PaintMaterialsRate: job.rate_mapa || 0,
|
||||
BodyHours: job.labhrs?.aggregate?.sum?.mod_lb_hrs || 0,
|
||||
BodyLaborRate: job.rate_lab || 0,
|
||||
TotalCostOfRepairs: (
|
||||
(job.job_totals?.totals?.subtotal?.amount || 0) / 100
|
||||
).toFixed(2),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const xml = create({ version: "1.0" }, header).end({ prettyPrint: true });
|
||||
const outputPath = path.join(config.path!, `PPGPaint.xml`);
|
||||
await fs.writeFile(outputPath, xml);
|
||||
log.info(`Saved PPG output XML to ${outputPath}`);
|
||||
} catch (error) {
|
||||
log.error(`Error generating PPG output for config ${config.id}:`, error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user