Further ESDP clean up.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -35,15 +35,15 @@ console.log(`ARTIFACT_SUFFIX set to: '${artifactSuffix}'`);
|
||||
if (process.argv.length > 2) {
|
||||
const command = process.argv[2];
|
||||
const args = process.argv.slice(3);
|
||||
|
||||
|
||||
console.log(`Executing: ${command} ${args.join(' ')}`);
|
||||
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, ARTIFACT_SUFFIX: artifactSuffix },
|
||||
shell: true
|
||||
});
|
||||
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import eslintPluginReactHooks from "eslint-plugin-react-hooks";
|
||||
import eslintPluginReactRefresh from "eslint-plugin-react-refresh";
|
||||
|
||||
export default tseslint.config(
|
||||
{ignores: ["**/node_modules", "**/dist", "**/out"]},
|
||||
{ ignores: ["**/node_modules", "**/dist", "**/out"] },
|
||||
tseslint.configs.recommended,
|
||||
eslintPluginReact.configs.flat.recommended,
|
||||
eslintPluginReact.configs.flat["jsx-runtime"],
|
||||
@@ -31,7 +31,7 @@ export default tseslint.config(
|
||||
{
|
||||
files: ["**/*.{js,mjs,ts,tsx,jsx,tsx}"],
|
||||
rules: {
|
||||
"prettier/prettier": ["error", {"endOfLine": "off"}]
|
||||
"prettier/prettier": ["error", { "endOfLine": "auto" }]
|
||||
}
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
|
||||
@@ -6,16 +6,9 @@ import fs from "fs";
|
||||
import _ from "lodash";
|
||||
import path from "path";
|
||||
import errorTypeCheck from "../../util/errorTypeCheck";
|
||||
import client from "../graphql/graphql-client";
|
||||
import {
|
||||
INSERT_AVAILABLE_JOB_TYPED,
|
||||
InsertAvailableJobResult,
|
||||
QUERY_JOB_BY_CLM_NO_TYPED,
|
||||
QUERY_VEHICLE_BY_VIN_TYPED,
|
||||
QueryJobByClmNoResult,
|
||||
VehicleQueryResult,
|
||||
} from "../graphql/queries";
|
||||
import { ScrubEstimate } from "../estimate-scrubber/estimate-scrubber";
|
||||
import store from "../store/store";
|
||||
import setAppProgressbar from "../util/setAppProgressBar";
|
||||
import DecodeAD1 from "./decode-ad1";
|
||||
import { DecodedAd1 } from "./decode-ad1.interface";
|
||||
import DecodeAD2 from "./decode-ad2";
|
||||
@@ -42,7 +35,6 @@ import DecodeTtl from "./decode-ttl";
|
||||
import { DecodedTtl } from "./decode-ttl.interface";
|
||||
import DecodeVeh from "./decode-veh";
|
||||
import { DecodedVeh } from "./decode-veh.interface";
|
||||
import setAppProgressbar from "../util/setAppProgressBar";
|
||||
import UploadEmsToS3 from "./emsbackup";
|
||||
|
||||
async function ImportJob(filepath: string): Promise<void> {
|
||||
@@ -142,39 +134,10 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
};
|
||||
setAppProgressbar(0.85);
|
||||
|
||||
const existingVehicleRecord: VehicleQueryResult = await client.request(
|
||||
QUERY_VEHICLE_BY_VIN_TYPED,
|
||||
{
|
||||
vin: jobObject.v_vin,
|
||||
},
|
||||
);
|
||||
|
||||
if (existingVehicleRecord.vehicles.length > 0) {
|
||||
delete newAvailableJob.est_data.vehicle;
|
||||
newAvailableJob.est_data.vehicleid = existingVehicleRecord.vehicles[0].id;
|
||||
}
|
||||
|
||||
console.log("Available Job record to upload;", newAvailableJob);
|
||||
|
||||
setAppProgressbar(0.95);
|
||||
if (jobObject.clm_no) {
|
||||
const existingJobRecord: QueryJobByClmNoResult = await client.request(
|
||||
QUERY_JOB_BY_CLM_NO_TYPED,
|
||||
{ clm_no: jobObject.clm_no },
|
||||
);
|
||||
|
||||
if (existingJobRecord.jobs.length > 0) {
|
||||
newAvailableJob.issupplement = true;
|
||||
newAvailableJob.jobid = existingJobRecord.jobs[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
const insertRecordResult: InsertAvailableJobResult = await client.request(
|
||||
INSERT_AVAILABLE_JOB_TYPED,
|
||||
{
|
||||
jobInput: [newAvailableJob],
|
||||
},
|
||||
);
|
||||
setAppProgressbar(-1);
|
||||
const uploadNotification = new Notification({
|
||||
title: "Job Imported",
|
||||
@@ -193,7 +156,8 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
});
|
||||
uploadNotification.show();
|
||||
|
||||
log.debug("Job inserted", insertRecordResult);
|
||||
//Scrub the estimate
|
||||
const scrubResults = ScrubEstimate({ job: jobObject });
|
||||
|
||||
UploadEmsToS3({
|
||||
extensionlessFilePath,
|
||||
@@ -216,7 +180,8 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
export default ImportJob;
|
||||
|
||||
export interface RawJobDataObject
|
||||
extends DecodedEnv,
|
||||
extends
|
||||
DecodedEnv,
|
||||
DecodedAd1,
|
||||
DecodedAD2,
|
||||
DecodedVeh,
|
||||
|
||||
186
src/main/estimate-scrubber/estimate-scrubber.ts
Normal file
186
src/main/estimate-scrubber/estimate-scrubber.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import log from "electron-log";
|
||||
import axios from "axios";
|
||||
import path from "path";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { promises as fsPromises } from "fs";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
// Function to write job object to logs subfolder
|
||||
async function writeJobToLogsFolder(job, fileName): Promise<string> {
|
||||
try {
|
||||
// Get the directory where electron-log stores its files
|
||||
const logFilePath = log.transports.file.getFile().path;
|
||||
const logsDir = path.dirname(logFilePath);
|
||||
|
||||
// Create a subfolder for job objects
|
||||
const jobLogsDir = path.join(logsDir, "esjson");
|
||||
|
||||
// Ensure the directory exists
|
||||
await fsPromises.mkdir(jobLogsDir, { recursive: true });
|
||||
|
||||
// Write the job object as JSON
|
||||
const jobFilePath = path.join(jobLogsDir, `${fileName}.json`);
|
||||
await fsPromises.writeFile(
|
||||
jobFilePath,
|
||||
JSON.stringify(job, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
log.debug(`Job object written to: ${jobFilePath}`);
|
||||
console.log(`Job object written to: ${jobFilePath}`);
|
||||
return jobFilePath;
|
||||
} catch (error) {
|
||||
log.error("Error writing job object to logs folder:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async function ScrubEstimate({ job }): Promise<string | undefined> {
|
||||
//These are hard coded as they are not secure values and checking happens based on other values.
|
||||
//No secret or private information is exposed.
|
||||
const basicAuthUser = "Imex2";
|
||||
const basicAuthpassword = "Patrick";
|
||||
const currentChannel = autoUpdater.channel;
|
||||
let estimateScrubberUrl;
|
||||
switch (currentChannel) {
|
||||
case "alpha":
|
||||
estimateScrubberUrl = "https://4284-79287.el-alt.com"; //dev specific URL.
|
||||
break;
|
||||
case "beta":
|
||||
estimateScrubberUrl = "https://4284-79073.el-alt.com"; //Beta specific URL.
|
||||
break;
|
||||
default:
|
||||
estimateScrubberUrl = "https://insurtechtoolkit.com"; //Production route.
|
||||
break;
|
||||
}
|
||||
|
||||
log.log(`Estimate Scrubber URL: [${currentChannel} |`, estimateScrubberUrl);
|
||||
const sendingEntityId = "87330f61-412b-4251-baaa-d026565b23c5";
|
||||
try {
|
||||
const esApiKey = job?.bodyshop?.es_api_key;
|
||||
//Perform data manipulation on the job object
|
||||
if (!job) {
|
||||
console.error("No job provided to ScrubEstimate");
|
||||
return;
|
||||
}
|
||||
|
||||
//Set shop metrics
|
||||
job.sending_entity_id = sendingEntityId;
|
||||
job.sending_entity_accept_terms_of_use = true;
|
||||
job.association_switch = "ATAM";
|
||||
job.rf_zip = job.bodyshop.zip_post;
|
||||
job.rf_ph1 = job.bodyshop.phone;
|
||||
job.g_ttl_amt = job.clm_total;
|
||||
job.source_system = "M"; //Requested by Steven.
|
||||
job.v_mileage = job.v_mileage?.toString() || ""; //Requested by Steven to be a string.
|
||||
|
||||
delete job.clm_total;
|
||||
delete job.bodyshop; //Bodyshop has to be passed through the object as we don't have access to the store here.
|
||||
|
||||
//Adjust the rates field to be MAT_TYPE instead of MATL_TYPE
|
||||
if (job.rates && Array.isArray(job.rates)) {
|
||||
job.rates.forEach((rate) => {
|
||||
if (rate.MATL_TYPE) {
|
||||
rate.MAT_TYPE = rate.MATL_TYPE;
|
||||
delete rate.MATL_TYPE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Lower case the rates & totals
|
||||
if (job.rates && Array.isArray(job.rates)) {
|
||||
job.rates = job.rates.map((rate) => {
|
||||
const lowercasedRate = {};
|
||||
for (const [key, value] of Object.entries(rate)) {
|
||||
lowercasedRate[key.toLowerCase()] = value;
|
||||
}
|
||||
return lowercasedRate;
|
||||
});
|
||||
}
|
||||
|
||||
if (job.totals && Array.isArray(job.totals)) {
|
||||
job.totals = job.totals.map((total) => {
|
||||
const lowercasedTotal = {};
|
||||
for (const [key, value] of Object.entries(total)) {
|
||||
lowercasedTotal[key.toLowerCase()] = value;
|
||||
}
|
||||
return lowercasedTotal;
|
||||
});
|
||||
}
|
||||
|
||||
const fileName = `${esApiKey}-${job.clm_no}-${Date.now()}`;
|
||||
|
||||
// Write job object to logs subfolder
|
||||
try {
|
||||
await writeJobToLogsFolder(job, fileName);
|
||||
} catch (error) {
|
||||
log.error("Failed to write job to logs folder:", error);
|
||||
// Continue with the rest of the function even if this fails
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
const jsonString = JSON.stringify(job);
|
||||
formData.append(
|
||||
"file",
|
||||
new Blob([jsonString], { type: "application/json" }),
|
||||
`${fileName}.json`,
|
||||
);
|
||||
|
||||
const result = await axios.post(
|
||||
`${estimateScrubberUrl}/api/sendems`,
|
||||
formData,
|
||||
{
|
||||
auth: {
|
||||
username: basicAuthUser,
|
||||
password: basicAuthpassword,
|
||||
},
|
||||
headers: {
|
||||
...(formData.getHeaders ? formData.getHeaders() : {}),
|
||||
APIkey: esApiKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const resultPDFUrl = result?.data?.report_link;
|
||||
const reportIssueUrl = `https://insurtechtoolkit.com/pcontactUs.aspx?apiKey=${esApiKey}&file=${fileName}.json`;
|
||||
// log.log("Estimate Scrubber Result:", result.data, resultPDFUrl);
|
||||
// const b = BrowserWindow.getAllWindows()[0];
|
||||
// b.webContents.send(ipcTypes.app.toRenderer.scrubResults, {
|
||||
// jobid: job.id,
|
||||
// items: result.data?.identified_item,
|
||||
// pdfUrl: resultPDFUrl,
|
||||
// reportIssueUrl,
|
||||
// });
|
||||
|
||||
// const pdfWindow = new BrowserWindow({
|
||||
|
||||
// webPreferences: {
|
||||
// plugins: true, // Enable PDF viewing
|
||||
// },
|
||||
// });
|
||||
|
||||
// pdfWindow.loadURL(resultPDFUrl);
|
||||
// pdfWindow.focus();
|
||||
return resultPDFUrl;
|
||||
} catch (error) {
|
||||
log.error("Error while scrubbing estimate:", error, error.stack);
|
||||
log.error("Error Response Data:", error.response?.data);
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
|
||||
if (error.status === 400) {
|
||||
mainWindow.webContents.send(ipcTypes.app.toRenderer.scrubError, {
|
||||
message:
|
||||
error.response?.data ||
|
||||
"Error encountered sending estimate to Estimate Scrubber.",
|
||||
});
|
||||
} else if (error.status === 401) {
|
||||
mainWindow.webContents.send(ipcTypes.app.toRenderer.scrubError, {
|
||||
message:
|
||||
"Authentication with Estimate Scrubber failed." ||
|
||||
error.response?.data,
|
||||
});
|
||||
}
|
||||
return "Error: Unable to scrub estimate.";
|
||||
}
|
||||
}
|
||||
|
||||
export { ScrubEstimate };
|
||||
@@ -1,67 +1,67 @@
|
||||
export interface User {
|
||||
stsTokenManager?: {
|
||||
accessToken: string;
|
||||
};
|
||||
stsTokenManager?: {
|
||||
accessToken: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BodyShop {
|
||||
shopname: string;
|
||||
id: string;
|
||||
shopname: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface GraphQLResponse {
|
||||
bodyshops_by_pk?: {
|
||||
imexshopid: string;
|
||||
shopname: string;
|
||||
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;
|
||||
};
|
||||
};
|
||||
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;
|
||||
};
|
||||
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;
|
||||
};
|
||||
};
|
||||
ins_co_nm: string;
|
||||
est_ct_ln: string;
|
||||
est_ct_fn: string;
|
||||
job_totals?: {
|
||||
rates?: {
|
||||
mapa?: {
|
||||
total?: {
|
||||
amount: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
totals?: {
|
||||
subtotal?: {
|
||||
amount: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
totals?: {
|
||||
subtotal?: {
|
||||
amount: number;
|
||||
};
|
||||
rate_mapa: number;
|
||||
labhrs_aggregate?: {
|
||||
aggregate?: {
|
||||
sum?: {
|
||||
mod_lb_hrs: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
rate_mapa: number;
|
||||
labhrs_aggregate?: {
|
||||
aggregate?: {
|
||||
sum?: {
|
||||
mod_lb_hrs: number;
|
||||
};
|
||||
rate_lab: number;
|
||||
}>;
|
||||
}
|
||||
};
|
||||
};
|
||||
rate_lab: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -27,16 +27,6 @@ const setSetting = async (
|
||||
return Store.get(`settings.${key}`);
|
||||
};
|
||||
|
||||
// Initialize paint scale input configs in store if not set
|
||||
if (!Store.get("settings.paintScaleInputConfigs")) {
|
||||
Store.set("settings.paintScaleInputConfigs", []);
|
||||
}
|
||||
|
||||
// Initialize paint scale output configs in store if not set
|
||||
if (!Store.get("settings.paintScaleOutputConfigs")) {
|
||||
Store.set("settings.paintScaleOutputConfigs", []);
|
||||
}
|
||||
|
||||
const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
|
||||
@@ -25,7 +25,7 @@ const ipcMainHandleAuthStateChanged = async (
|
||||
checkForAppUpdatesContinuously();
|
||||
};
|
||||
|
||||
async function setReleaseChannel() {
|
||||
async function setReleaseChannel(): Promise<void> {
|
||||
try {
|
||||
//Need to query the currently active shop, and store the metadata as well.
|
||||
//Also need to query the OP Codes for decoding reference.
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
import log from "electron-log/main";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import axios from "axios";
|
||||
import { create } from "xmlbuilder2";
|
||||
import { parseStringPromise } from "xml2js";
|
||||
import store from "../../store/store";
|
||||
import client, { getTokenFromRenderer } from "../../graphql/graphql-client";
|
||||
import { PaintScaleConfig } from "../../../util/types/paintScale";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
PPG_DATA_QUERY_TYPED,
|
||||
PpgDataQueryResult,
|
||||
PpgDataQueryVariables,
|
||||
} from "../../graphql/queries";
|
||||
|
||||
export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
|
||||
try {
|
||||
log.info(
|
||||
`Polling input directory for PPG config ${config.id}: ${config.path}`,
|
||||
);
|
||||
|
||||
log.debug(
|
||||
`Archive dir: ${path.join(config.path!, "archive")}, Error dir: ${path.join(config.path!, "error")}`,
|
||||
);
|
||||
|
||||
// Ensure archive and error directories exist
|
||||
const archiveDir = path.join(config.path!, "archive");
|
||||
const errorDir = path.join(config.path!, "error");
|
||||
try {
|
||||
await fs.mkdir(archiveDir, { recursive: true });
|
||||
await fs.mkdir(errorDir, { recursive: true });
|
||||
log.debug(
|
||||
`Archive and error directories ensured: ${archiveDir}, ${errorDir}`,
|
||||
);
|
||||
} catch (dirError) {
|
||||
log.error(`Failed to create directories for ${config.path}:`, dirError);
|
||||
throw dirError;
|
||||
}
|
||||
|
||||
// Check for files
|
||||
const files = await fs.readdir(config.path!);
|
||||
log.debug(`Found ${files.length} files in ${config.path}:`, files);
|
||||
|
||||
for (const file of files) {
|
||||
// Only process XML files
|
||||
if (!file.toLowerCase().endsWith(".xml")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(config.path!, file);
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
if (!stats.isFile()) {
|
||||
continue;
|
||||
}
|
||||
} catch (statError) {
|
||||
log.warn(`Failed to stat file ${filePath}:`, statError);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debug(`Processing input file: ${filePath}`);
|
||||
|
||||
// Check file accessibility (e.g., not locked)
|
||||
try {
|
||||
await fs.access(filePath, fs.constants.R_OK);
|
||||
} catch (error) {
|
||||
log.warn(`File ${filePath} is inaccessible, skipping:`, error);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate XML structure
|
||||
let xmlContent: BlobPart;
|
||||
try {
|
||||
xmlContent = await fs.readFile(filePath, "utf8");
|
||||
await parseStringPromise(xmlContent);
|
||||
log.debug(`Successfully validated XML for ${filePath}`);
|
||||
} catch (error) {
|
||||
log.error(`Invalid XML in ${filePath}:`, error);
|
||||
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
||||
const originalFilename = path.basename(file, path.extname(file));
|
||||
const errorPath = path.join(
|
||||
errorDir,
|
||||
`${originalFilename}-${timestamp}.xml`,
|
||||
);
|
||||
try {
|
||||
await fs.rename(filePath, errorPath);
|
||||
log.debug(`Moved invalid file to error: ${errorPath}`);
|
||||
} catch (moveError) {
|
||||
log.error(
|
||||
`Failed to move invalid file to error directory ${errorPath}:`,
|
||||
moveError,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get authentication token
|
||||
let token: string | null;
|
||||
try {
|
||||
token = await getTokenFromRenderer();
|
||||
if (!token) {
|
||||
log.error(`No authentication token for file: ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
log.debug(
|
||||
`Obtained authentication token for ${filePath}: ${token.slice(0, 10)}...`,
|
||||
);
|
||||
} catch (tokenError) {
|
||||
log.error(
|
||||
`Failed to obtain authentication token for ${filePath}:`,
|
||||
tokenError,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Upload file to API
|
||||
const formData = new FormData();
|
||||
formData.append("file", new Blob([xmlContent]), path.basename(filePath));
|
||||
const shopId = (store.get("app.bodyshop") as any)?.shopname || "";
|
||||
formData.append("shopId", shopId);
|
||||
log.debug(`Shop ID: ${shopId}`);
|
||||
|
||||
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}`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(finalUrl, formData, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
timeout: 10000, // 10-second timeout
|
||||
});
|
||||
|
||||
log.info(`Upload response for ${filePath}:`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
log.info(`Successful upload of ${filePath}`);
|
||||
// Move file to archive
|
||||
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
|
||||
const originalFilename = path.basename(file, path.extname(file));
|
||||
const archivePath = path.join(
|
||||
archiveDir,
|
||||
`${originalFilename}-${timestamp}.xml`,
|
||||
);
|
||||
try {
|
||||
await fs.access(archiveDir, fs.constants.W_OK); // Verify archiveDir is writable
|
||||
await fs.rename(filePath, archivePath);
|
||||
log.info(`Moved file to archive: ${archivePath}`);
|
||||
} catch (moveError) {
|
||||
log.error(
|
||||
`Failed to move file to archive directory ${archivePath}:`,
|
||||
moveError,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log.error(
|
||||
`Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
|
||||
{ responseData: response.data },
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
log.error(`Error uploading ${filePath}:`, {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response
|
||||
? {
|
||||
status: error.response.status,
|
||||
statusText: error.response.statusText,
|
||||
data: error.response.data,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
} 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 variables: PpgDataQueryVariables = {
|
||||
today: dayjs().toISOString(),
|
||||
todayplus5: dayjs().add(5, "day").toISOString(),
|
||||
shopid: (store.get("app.bodyshop") as any)?.id,
|
||||
};
|
||||
|
||||
const response = await client.request<
|
||||
PpgDataQueryResult,
|
||||
PpgDataQueryVariables
|
||||
>(PPG_DATA_QUERY_TYPED, variables);
|
||||
const jobs = response.jobs ?? [];
|
||||
|
||||
const header = {
|
||||
PPG: {
|
||||
Header: {
|
||||
Protocol: {
|
||||
Message: "PaintShopInterface",
|
||||
Name: "PPG",
|
||||
Version: "1.5.0",
|
||||
},
|
||||
Transaction: {
|
||||
TransactionID: "",
|
||||
TransactionDate: dayjs().format("YYYY-MM-DD:HH:mm"),
|
||||
},
|
||||
Product: {
|
||||
Name: import.meta.env.VITE_COMPANY === "IMEX",
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user