260 lines
7.9 KiB
TypeScript
260 lines
7.9 KiB
TypeScript
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 from "../../graphql/graphql-client";
|
|
import { PaintScaleConfig } 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 and error directories exist
|
|
const archiveDir = path.join(config.path!, "archive");
|
|
const errorDir = path.join(config.path!, "error");
|
|
await fs.mkdir(archiveDir, { recursive: true });
|
|
await fs.mkdir(errorDir, { 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()) {
|
|
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);
|
|
} catch (error) {
|
|
log.error(`Invalid XML in ${filePath}:`, error);
|
|
const errorPath = path.join(errorDir, path.basename(filePath));
|
|
await fs.rename(filePath, errorPath);
|
|
log.debug(`Moved invalid file to error: ${errorPath}`);
|
|
continue;
|
|
}
|
|
|
|
// Get authentication token
|
|
const token = (store.get("user") as any)?.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([xmlContent]), 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}`);
|
|
|
|
try {
|
|
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.status} ${response.statusText}`,
|
|
response.data,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
log.error(`Error uploading ${filePath}:`, error);
|
|
}
|
|
}
|
|
} 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: (() => {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = ("0" + (now.getMonth() + 1)).slice(-2);
|
|
const day = ("0" + now.getDate()).slice(-2);
|
|
const hours = ("0" + now.getHours()).slice(-2);
|
|
const minutes = ("0" + now.getMinutes()).slice(-2);
|
|
return `${year}-${month}-${day}:${hours}:${minutes}`;
|
|
})(),
|
|
},
|
|
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);
|
|
}
|
|
}
|
|
|