607 lines
16 KiB
TypeScript
607 lines
16 KiB
TypeScript
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 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 => {
|
|
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: 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."),
|
|
);
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
|
|
logIpcMessages(); |