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 > = { [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 > = { [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 => { 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();