feature/IO-3205-Paint-Scale-Integrations: Cleanup refactor checkpoint

This commit is contained in:
Dave Richer
2025-04-30 13:17:58 -04:00
parent cba0a6ed08
commit 90077fc72c
9 changed files with 294 additions and 328 deletions

View File

@@ -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<void>
const inputTypeHandlers: Partial<
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) {
// 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<void>
const outputTypeHandlers: Partial<
Record<PaintScaleType, (config: PaintScaleConfig) => Promise<void>>
> = {
[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

View 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;
}>;
}

View File

@@ -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

View File

@@ -1,11 +0,0 @@
// src/types/paintScale.ts
export enum PaintScaleType {
PPG = 'PPG',
}
export interface PaintScaleConfig {
id: string;
path?: string;
type: PaintScaleType;
pollingInterval: number;
}

View File

@@ -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<void> {
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<void> {
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);
}
}

View File

@@ -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
};

View File

@@ -21,7 +21,7 @@ import {
PaintScaleConfig,
PaintScaleType,
paintScaleTypeOptions,
} from "./PaintScale/types";
} from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleInputPaths: FC = () => {

View File

@@ -11,7 +11,7 @@ import {
PaintScaleConfig,
PaintScaleType,
paintScaleTypeOptions,
} from "./PaintScale/types";
} from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleOutputPaths: FC = () => {

View File

@@ -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) => ({
export interface PaintScaleConfig {
id: string;
path?: string;
type: PaintScaleType;
pollingInterval: number;
}
export const paintScaleTypeOptions = Object.values(PaintScaleType).map(
(type) => ({
value: type,
label: type,
}));
}),
);