feature/IO-3205-Paint-Scale-Integrations: Logic and Cron in place

This commit is contained in:
Dave Richer
2025-04-28 10:09:22 -04:00
parent c73db112c7
commit fa0c6f7db3
6 changed files with 696 additions and 69 deletions

View File

@@ -1,4 +1,3 @@
// main/ipcMainConfig.ts
import { app, ipcMain } from "electron";
import log from "electron-log/main";
import { autoUpdater } from "electron-updater";
@@ -8,6 +7,14 @@ import ImportJob from "../decoder/decoder";
import store from "../store/store";
import { StartWatcher, StopWatcher } from "../watcher/watcher";
import {
SettingEmsOutFilePathGet,
SettingEmsOutFilePathSet,
SettingsPaintScaleInputConfigsGet,
SettingsPaintScaleInputConfigsSet,
SettingsPaintScaleInputPathSet,
SettingsPaintScaleOutputConfigsGet,
SettingsPaintScaleOutputConfigsSet,
SettingsPaintScaleOutputPathSet,
SettingsPpcFilePathGet,
SettingsPpcFilePathSet,
SettingsWatchedFilePathsAdd,
@@ -15,19 +22,84 @@ import {
SettingsWatchedFilePathsRemove,
SettingsWatcherPollingGet,
SettingsWatcherPollingSet,
SettingEmsOutFilePathSet,
SettingEmsOutFilePathGet,
SettingsPaintScaleInputConfigsGet,
SettingsPaintScaleInputConfigsSet,
SettingsPaintScaleInputPathSet,
SettingsPaintScaleOutputConfigsGet,
SettingsPaintScaleOutputConfigsSet,
SettingsPaintScaleOutputPathSet,
} from "./ipcMainHandler.settings";
import {
ipcMainHandleAuthStateChanged,
ipMainHandleResetPassword,
} from "./ipcMainHandler.user";
import cron from "node-cron";
import fs from "fs/promises";
import axios from "axios";
import { create } from "xmlbuilder2";
import client from "../graphql/graphql-client";
import { PaintScaleConfig, PaintScaleType } from "./paintScale";
// Add these interfaces at the top of your file
interface User {
stsTokenManager?: {
accessToken: string;
};
}
interface BodyShop {
shopname: string;
id: string;
}
interface GraphQLResponse {
bodyshops_by_pk?: {
imexshopid: string;
shopname: string;
};
jobs?: Array<{
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;
}>;
}
// Log all IPC messages and their payloads
const logIpcMessages = (): void => {
@@ -39,10 +111,10 @@ const logIpcMessages = (): void => {
}
ipcMain.on(messageType, (event, payload) => {
log.info(
`%c[IPC Main]%c${messageType}`,
"color: red",
"color: green",
payload,
`%c[IPC Main]%c${messageType}`,
"color: red",
"color: green",
payload,
);
if (originalHandler) {
originalHandler(event, payload);
@@ -51,8 +123,354 @@ const logIpcMessages = (): void => {
});
};
// Input handler map
const inputTypeHandlers: Record<
PaintScaleType,
(config: PaintScaleConfig) => Promise<void>
> = {
[PaintScaleType.PPG]: async (config: PaintScaleConfig) => {
try {
log.info(
`Polling input directory for PPG config ${config.id}: ${config.path}`,
);
// Ensure archive directory exists
const archiveDir = path.join(config.path!, "archive");
await fs.mkdir(archiveDir, { recursive: true });
// Check for files
const files = await fs.readdir(config.path!);
for (const file of files) {
const filePath = path.join(config.path!, file);
const stats = await fs.stat(filePath);
if (stats.isFile()) {
log.debug(`Processing input file: ${filePath}`);
// Get authentication token
const token = (store.get("user") as User)?.stsTokenManager
?.accessToken;
if (!token) {
log.error(`No authentication token for file: ${filePath}`);
continue;
}
// Upload file to API
const formData = new FormData();
formData.append(
"file",
new Blob([await fs.readFile(filePath)]),
path.basename(filePath),
);
formData.append(
"shopId",
(store.get("app.bodyshop") as BodyShop)?.shopname || "",
);
const response = await axios.post(
"https://your-api-base-url/mixdata/upload",
formData,
{
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
},
);
if (response.status === 200) {
log.info(`Successful upload of ${filePath}`);
// Move file to archive
const archivePath = path.join(archiveDir, path.basename(filePath));
await fs.rename(filePath, archivePath);
log.debug(`Moved file to archive: ${archivePath}`);
} else {
log.error(`Failed to upload ${filePath}: ${response.statusText}`);
}
}
}
} catch (error) {
log.error(`Error polling input directory ${config.path}:`, error);
}
},
// Add other input type handlers as needed
};
// Output handler map
const outputTypeHandlers: Record<
PaintScaleType,
(config: PaintScaleConfig) => Promise<void>
> = {
[PaintScaleType.PPG]: async (config: PaintScaleConfig) => {
try {
log.info(`Generating PPG output for config ${config.id}: ${config.path}`);
// Ensure output directory exists
await fs.mkdir(config.path!, { recursive: true });
// GraphQL query for jobs
const query = `
query PpgData($today: date!, $todayplus5: date!, $shopid: uuid!) {
bodyshops_by_pk(id: $shopid) {
imexshopid
shopname
}
jobs(where: {
_and: [
{ shop_id: { _eq: $shopid } },
{ _or: [
{ status: { _eq: "Active" } },
{ scheduled_date: { _gte: $today, _lte: $todayplus5 } }
] }
]
}) {
ro_number
ownr_ln
ownr_fn
plate_no
v_vin
v_model_yr
v_make_desc
v_model_desc
vehicle {
v_paint_codes {
paint_cd1
}
}
larhrs_aggregate: estimate_lines_aggregate(where: { line_type: { _eq: "Paint" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
ins_co_nm
est_ct_ln
est_ct_fn
job_totals {
rates {
mapa {
total {
amount
}
}
}
totals {
subtotal {
amount
}
}
}
rate_mapa
labhrs_aggregate: estimate_lines_aggregate(where: { line_type: { _eq: "Body" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
rate_lab
}
}
`;
const variables = {
today: new Date().toISOString().split("T")[0],
todayplus5: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000)
.toISOString()
.split("T")[0],
shopid: (store.get("app.bodyshop") as BodyShop)?.id,
};
const response = (await client.request(
query,
variables,
)) as GraphQLResponse;
// Generate XML
const doc = create({ version: "1.0" })
.ele("PPG")
.ele("Header")
.ele("Protocol")
.ele("Message")
.txt("PaintShopInterface")
.up()
.ele("Name")
.txt("PPG")
.up()
.ele("Version")
.txt("1.5.0")
.up()
.up()
.ele("Transaction")
.ele("TransactionID")
.txt("")
.up()
.ele("TransactionDate")
.txt(new Date().toISOString().replace("T", " ").substring(0, 19))
.up()
.up()
.ele("Product")
.ele("Name")
.txt("ImEX Online")
.up()
.ele("Version")
.txt("")
.up()
.up()
.up()
.ele("DataInterface")
.ele("ROData")
.ele("ShopInfo")
.ele("ShopID")
.txt(response.bodyshops_by_pk?.imexshopid || "")
.up()
.ele("ShopName")
.txt(response.bodyshops_by_pk?.shopname || "")
.up()
.up()
.ele("RepairOrders")
.ele("ROCount")
.txt((response.jobs?.length || 0).toString())
.up()
.up()
.up()
.up();
const repairOrders = (doc.root() as any).findOne("RepairOrders");
(response as GraphQLResponse).jobs?.forEach((job: any) => {
repairOrders
.ele("RO")
.ele("RONumber")
.txt(job.ro_number || "")
.up()
.ele("ROStatus")
.txt("Open")
.up()
.ele("Customer")
.txt(`${job.ownr_ln || ""}, ${job.ownr_fn || ""}`)
.up()
.ele("ROPainterNotes")
.txt("")
.up()
.ele("LicensePlateNum")
.txt(job.plate_no || "")
.up()
.ele("VIN")
.txt(job.v_vin || "")
.up()
.ele("ModelYear")
.txt(job.v_model_yr || "")
.up()
.ele("MakeDesc")
.txt(job.v_make_desc || "")
.up()
.ele("ModelName")
.txt(job.v_model_desc || "")
.up()
.ele("OEMColorCode")
.txt(job.vehicle?.v_paint_codes?.paint_cd1 || "")
.up()
.ele("RefinishLaborHours")
.txt(job.larhrs_aggregate?.aggregate?.sum?.mod_lb_hrs || 0)
.up()
.ele("InsuranceCompanyName")
.txt(job.ins_co_nm || "")
.up()
.ele("EstimatorName")
.txt(`${job.est_ct_ln || ""}, ${job.est_ct_fn || ""}`)
.up()
.ele("PaintMaterialsRevenue")
.txt(
((job.job_totals?.rates?.mapa?.total?.amount || 0) / 100).toFixed(
2,
),
)
.up()
.ele("PaintMaterialsRate")
.txt(job.rate_mapa || 0)
.up()
.ele("BodyHours")
.txt(job.labhrs_aggregate?.aggregate?.sum?.mod_lb_hrs || 0)
.up()
.ele("BodyLaborRate")
.txt(job.rate_lab || 0)
.up()
.ele("TotalCostOfRepairs")
.txt(
((job.job_totals?.totals?.subtotal?.amount || 0) / 100).toFixed(2),
)
.up();
});
// Save XML to file
const xmlString = doc.end({ prettyPrint: true });
const outputPath = path.join(config.path!, "PPGPaint.xml");
await fs.writeFile(outputPath, xmlString);
log.info(`Saved PPG output XML to ${outputPath}`);
} catch (error) {
log.error(`Error generating PPG output for config ${config.id}:`, error);
}
},
// 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}s`,
);
});
};
// 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}s`,
);
});
};
// Existing IPC handlers...
ipcMain.on(ipcTypes.toMain.test, () =>
console.log("** Verify that ipcMain is loaded and working."),
console.log("** Verify that ipcMain is loaded and working."),
);
// Auth handler
@@ -80,63 +498,86 @@ if (import.meta.env.DEV) {
// Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.get,
SettingsWatchedFilePathsGet,
ipcTypes.toMain.settings.filepaths.get,
SettingsWatchedFilePathsGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.add,
SettingsWatchedFilePathsAdd,
ipcTypes.toMain.settings.filepaths.add,
SettingsWatchedFilePathsAdd,
);
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.remove,
SettingsWatchedFilePathsRemove,
ipcTypes.toMain.settings.filepaths.remove,
SettingsWatchedFilePathsRemove,
);
ipcMain.handle(
ipcTypes.toMain.settings.watcher.getpolling,
SettingsWatcherPollingGet,
ipcTypes.toMain.settings.watcher.getpolling,
SettingsWatcherPollingGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.watcher.setpolling,
SettingsWatcherPollingSet,
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,
ipcTypes.toMain.settings.getEmsOutFilePath,
SettingEmsOutFilePathGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.setEmsOutFilePath,
SettingEmsOutFilePathSet,
ipcTypes.toMain.settings.setEmsOutFilePath,
SettingEmsOutFilePathSet,
);
// Paint Scale Input Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.getInputConfigs,
SettingsPaintScaleInputConfigsGet,
ipcTypes.toMain.settings.paintScale.getInputConfigs,
SettingsPaintScaleInputConfigsGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setInputConfigs,
SettingsPaintScaleInputConfigsSet,
ipcTypes.toMain.settings.paintScale.setInputConfigs,
SettingsPaintScaleInputConfigsSet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setInputPath,
SettingsPaintScaleInputPathSet,
ipcTypes.toMain.settings.paintScale.setInputPath,
SettingsPaintScaleInputPathSet,
);
// Paint Scale Output Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
SettingsPaintScaleOutputConfigsGet,
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
SettingsPaintScaleOutputConfigsGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setOutputConfigs,
SettingsPaintScaleOutputConfigsSet,
ipcTypes.toMain.settings.paintScale.setOutputConfigs,
SettingsPaintScaleOutputConfigsSet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setOutputPath,
SettingsPaintScaleOutputPathSet,
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, () => {
@@ -145,16 +586,22 @@ ipcMain.handle(ipcTypes.toMain.user.getActiveShop, () => {
// Watcher Handlers
ipcMain.on(ipcTypes.toMain.watcher.start, () => {
StartWatcher();
StartWatcher().catch((error) => {
log.error("Error starting watcher:", error);
});
});
ipcMain.on(ipcTypes.toMain.watcher.stop, () => {
StopWatcher();
StopWatcher().catch((error) => {
log.error("Error stopping watcher:", error);
});
});
ipcMain.on(ipcTypes.toMain.updates.download, () => {
log.info("Download update requested from renderer.");
autoUpdater.downloadUpdate();
autoUpdater.downloadUpdate().catch((error) => {
log.error("Error downloading update:", error);
});
});
logIpcMessages();

View File

@@ -0,0 +1,17 @@
// src/types/paintScale.ts
export enum PaintScaleType {
PPG = 'PPG',
// Add other types as needed
}
export interface PaintScaleConfig {
id: string;
path: string | null;
type: PaintScaleType;
pollingInterval: number;
}
export const paintScaleTypeOptions = [
{ value: PaintScaleType.PPG, label: 'PPG' },
// Add other options as needed
];

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { PaintScaleConfig, PaintScaleType } from './types';
import ipcTypes from '../../../../../util/ipcTypes.json';
import {PaintScaleConfig, PaintScaleType} from "./types";
type ConfigType = 'input' | 'output';
@@ -28,7 +28,7 @@ export const usePaintScaleConfig = (configType: ConfigType) => {
// Ensure all configs have a pollingInterval and type (for backward compatibility)
const updatedConfigs = configs.map(config => ({
...config,
pollingInterval: config.pollingInterval || 1440, // Default to 1440 seconds if not set
pollingInterval: config.pollingInterval || 1440, // Default to 1440 seconds
type: config.type || PaintScaleType.PPG, // Default type if missing
}));
setPaintScaleConfigs(updatedConfigs || []);
@@ -38,10 +38,18 @@ export const usePaintScaleConfig = (configType: ConfigType) => {
});
}, [getConfigsMethod]);
// Save configs to store
// Save configs to store and notify main process of config changes
const saveConfigs = (configs: PaintScaleConfig[]) => {
window.electron.ipcRenderer
.invoke(setConfigsMethod, configs)
.then(() => {
// Notify main process to update cron job
if (configType === 'input') {
window.electron.ipcRenderer.send(ipcTypes.toMain.settings.paintScale.updateInputCron, configs);
} else if (configType === 'output') {
window.electron.ipcRenderer.send(ipcTypes.toMain.settings.paintScale.updateOutputCron, configs);
}
})
.catch((error) => {
console.error(`Failed to save paint scale ${configType} configs:`, error);
});

View File

@@ -34,7 +34,9 @@
"setInputPath": "toMain_settings_paintScale_setInputPath",
"getOutputConfigs": "toMain_settings_paintScale_getOutputConfigs",
"setOutputConfigs": "toMain_settings_paintScale_setOutputConfigs",
"setOutputPath": "toMain_settings_paintScale_setOutputPath"
"setOutputPath": "toMain_settings_paintScale_setOutputPath",
"updateInputCron": "toMain_settings_paintScale_updateInputCron",
"updateOutputCron": "toMain_settings_paintScale_updateOutputCron"
}
},
"user": {