From 90077fc72cc7da912508155dba14d99548e8041a Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 30 Apr 2025 13:17:58 -0400 Subject: [PATCH] feature/IO-3205-Paint-Scale-Integrations: Cleanup refactor checkpoint --- src/main/ipc/ipcMainConfig.ts | 308 +----------------- src/main/ipc/ipcMainConfig.types.ts | 67 ++++ src/main/ipc/ipcMainHandler.settings.ts | 2 +- src/main/ipc/paintScale.ts | 11 - src/main/ipc/paintScaleHandlers/PPG.ts | 201 ++++++++++++ .../PaintScale/usePaintScaleConfig.ts | 3 +- .../Settings.PaintScaleInputPaths.tsx | 2 +- .../Settings.PaintScaleOutputPaths.tsx | 2 +- .../types.ts => util/types/paintScale.ts} | 26 +- 9 files changed, 294 insertions(+), 328 deletions(-) create mode 100644 src/main/ipc/ipcMainConfig.types.ts delete mode 100644 src/main/ipc/paintScale.ts create mode 100644 src/main/ipc/paintScaleHandlers/PPG.ts rename src/{renderer/src/components/Settings/PaintScale/types.ts => util/types/paintScale.ts} (54%) diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts index 739c62d..fba4f99 100644 --- a/src/main/ipc/ipcMainConfig.ts +++ b/src/main/ipc/ipcMainConfig.ts @@ -28,80 +28,8 @@ import { 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<{ - 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; - }>; -} +import { PaintScaleConfig, PaintScaleType } from "../../util/types/paintScale"; +import { ppgInputHandler, ppgOutputHandler } from "./paintScaleHandlers/PPG"; const initializeCronTasks = async () => { try { @@ -144,237 +72,19 @@ const logIpcMessages = (): void => { }; // Input handler map -const inputTypeHandlers: Record< - PaintScaleType, - (config: PaintScaleConfig) => Promise +const inputTypeHandlers: Partial< + Record 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) { - // Only process XML files - if (!file.toLowerCase().endsWith(".xml")) { - continue; - } - - 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 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}`); - - const response = await axios.post(finalUrl, 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); - } - }, + [PaintScaleType.PPG]: ppgInputHandler, // Add other input type handlers as needed }; // Output handler map -const outputTypeHandlers: Record< - PaintScaleType, - (config: PaintScaleConfig) => Promise +const outputTypeHandlers: Partial< + Record Promise> > = { - [PaintScaleType.PPG]: async (config: PaintScaleConfig) => { - try { - log.info(`Generating PPG output for config ${config.id}: ${config.path}`); - - await fs.mkdir(config.path!, { recursive: true }); - - const query = ` - 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 - } - } - } - } - } -`; - - const variables = { - today: new Date().toISOString(), - todayplus5: new Date(Date.now() + 5 * 86400000).toISOString(), - shopid: (store.get("app.bodyshop") as BodyShop)?.id, - }; - - const response = (await client.request( - query, - variables, - )) as GraphQLResponse; - - const jobs = response.jobs ?? []; - - const header = { - PPG: { - Header: { - Protocol: { - Message: "PaintShopInterface", - Name: "PPG", - Version: "1.5.0", - }, - Transaction: { - TransactionID: "", - TransactionDate: new Date() - .toISOString() - .replace("T", " ") - .substring(0, 19), - }, - Product: { - Name: "ImEX Online", - 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); - } - }, + [PaintScaleType.PPG]: ppgOutputHandler, + // Add other output type handlers as needed }; // Default handler for unsupported types diff --git a/src/main/ipc/ipcMainConfig.types.ts b/src/main/ipc/ipcMainConfig.types.ts new file mode 100644 index 0000000..c90c9b4 --- /dev/null +++ b/src/main/ipc/ipcMainConfig.types.ts @@ -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; + }>; +} \ No newline at end of file diff --git a/src/main/ipc/ipcMainHandler.settings.ts b/src/main/ipc/ipcMainHandler.settings.ts index b87dfe2..b71b636 100644 --- a/src/main/ipc/ipcMainHandler.settings.ts +++ b/src/main/ipc/ipcMainHandler.settings.ts @@ -10,7 +10,7 @@ import { StartWatcher, StopWatcher, } from "../watcher/watcher"; -import { PaintScaleConfig } from "./paintScale"; +import { PaintScaleConfig } from "../../util/types/paintScale"; // Initialize paint scale input configs in store if not set diff --git a/src/main/ipc/paintScale.ts b/src/main/ipc/paintScale.ts deleted file mode 100644 index 806d13f..0000000 --- a/src/main/ipc/paintScale.ts +++ /dev/null @@ -1,11 +0,0 @@ -// src/types/paintScale.ts -export enum PaintScaleType { - PPG = 'PPG', -} - -export interface PaintScaleConfig { - id: string; - path?: string; - type: PaintScaleType; - pollingInterval: number; -} \ No newline at end of file diff --git a/src/main/ipc/paintScaleHandlers/PPG.ts b/src/main/ipc/paintScaleHandlers/PPG.ts new file mode 100644 index 0000000..32dad2a --- /dev/null +++ b/src/main/ipc/paintScaleHandlers/PPG.ts @@ -0,0 +1,201 @@ +import log from "electron-log/main"; +import path from "path"; +import fs from "fs/promises"; +import axios from "axios"; +import { create } from "xmlbuilder2"; +import store from "../../store/store"; +import client from "../../graphql/graphql-client"; +import { PaintScaleConfig, PaintScaleType } from "../../util/types/paintScale"; + +// PPG Input Handler +export async function ppgInputHandler(config: PaintScaleConfig): Promise { + 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) { + if (!file.toLowerCase().endsWith(".xml")) continue; + + 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 any)?.stsTokenManager?.accessToken; + if (!token) { + log.error(`No authentication token for file: ${filePath}`); + continue; + } + + const formData = new FormData(); + formData.append("file", new Blob([await fs.readFile(filePath)]), path.basename(filePath)); + formData.append("shopId", (store.get("app.bodyshop") as any)?.shopname || ""); + + 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}`); + + const response = await axios.post(finalUrl, 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); + } +} + +// PPG Output Handler +export async function ppgOutputHandler(config: PaintScaleConfig): Promise { + try { + log.info(`Generating PPG output for config ${config.id}: ${config.path}`); + + await fs.mkdir(config.path!, { recursive: true }); + + const query = ` + 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 + } + } + } + } + } +`; + + const variables = { + today: new Date().toISOString(), + todayplus5: new Date(Date.now() + 5 * 86400000).toISOString(), + shopid: (store.get("app.bodyshop") as any)?.id, + }; + + const response = (await client.request(query, variables)) as any; + const jobs = response.jobs ?? []; + + const header = { + PPG: { + Header: { + Protocol: { + Message: "PaintShopInterface", + Name: "PPG", + Version: "1.5.0", + }, + Transaction: { + TransactionID: "", + TransactionDate: new Date().toISOString().replace("T", " ").substring(0, 19), + }, + Product: { + Name: "ImEX Online", + 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: any) => ({ + 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); + } +} diff --git a/src/renderer/src/components/Settings/PaintScale/usePaintScaleConfig.ts b/src/renderer/src/components/Settings/PaintScale/usePaintScaleConfig.ts index a79bf0d..f87be8f 100644 --- a/src/renderer/src/components/Settings/PaintScale/usePaintScaleConfig.ts +++ b/src/renderer/src/components/Settings/PaintScale/usePaintScaleConfig.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import ipcTypes from '../../../../../util/ipcTypes.json'; -import { PaintScaleConfig, PaintScaleType } from "./types"; +import { PaintScaleConfig, PaintScaleType } from '../../../../../util/types/paintScale'; import { message } from "antd"; import {useTranslation} from "react-i18next"; @@ -76,7 +76,6 @@ export const usePaintScaleConfig = (configType: ConfigType) => { const handleAddConfig = (type: PaintScaleType) => { const newConfig: PaintScaleConfig = { id: Date.now().toString(), - path: null, type, pollingInterval: 1440, // Default to 1440 seconds }; diff --git a/src/renderer/src/components/Settings/Settings.PaintScaleInputPaths.tsx b/src/renderer/src/components/Settings/Settings.PaintScaleInputPaths.tsx index 187ff77..444047b 100644 --- a/src/renderer/src/components/Settings/Settings.PaintScaleInputPaths.tsx +++ b/src/renderer/src/components/Settings/Settings.PaintScaleInputPaths.tsx @@ -21,7 +21,7 @@ import { PaintScaleConfig, PaintScaleType, paintScaleTypeOptions, -} from "./PaintScale/types"; +} from "../../../../util/types/paintScale"; import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig"; const SettingsPaintScaleInputPaths: FC = () => { diff --git a/src/renderer/src/components/Settings/Settings.PaintScaleOutputPaths.tsx b/src/renderer/src/components/Settings/Settings.PaintScaleOutputPaths.tsx index 60da7e6..d3f695d 100644 --- a/src/renderer/src/components/Settings/Settings.PaintScaleOutputPaths.tsx +++ b/src/renderer/src/components/Settings/Settings.PaintScaleOutputPaths.tsx @@ -11,7 +11,7 @@ import { PaintScaleConfig, PaintScaleType, paintScaleTypeOptions, -} from "./PaintScale/types"; +} from "../../../../util/types/paintScale"; import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig"; const SettingsPaintScaleOutputPaths: FC = () => { diff --git a/src/renderer/src/components/Settings/PaintScale/types.ts b/src/util/types/paintScale.ts similarity index 54% rename from src/renderer/src/components/Settings/PaintScale/types.ts rename to src/util/types/paintScale.ts index cdbbb13..a3fd053 100644 --- a/src/renderer/src/components/Settings/PaintScale/types.ts +++ b/src/util/types/paintScale.ts @@ -1,17 +1,17 @@ -export interface PaintScaleConfig { - id: string; - path: string | null; - type: PaintScaleType; - pollingInterval: number; // In seconds -} - export enum PaintScaleType { PPG = "PPG", - SHERWIN = "SHERWIN", - AKZO = "AKZO", } -export const paintScaleTypeOptions = Object.values(PaintScaleType).map((type) => ({ - value: type, - label: type, -})); +export interface PaintScaleConfig { + id: string; + path?: string; + type: PaintScaleType; + pollingInterval: number; +} + +export const paintScaleTypeOptions = Object.values(PaintScaleType).map( + (type) => ({ + value: type, + label: type, + }), +);