Strip out partner related functionality.

This commit is contained in:
Patrick Fic
2025-12-08 13:53:15 -08:00
parent 267ef714a7
commit 39d81bbc6a
54 changed files with 79 additions and 3889 deletions

View File

@@ -1,158 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { ad1FieldLineDescriptors } from "../util/ems-interface/fielddescriptors/ad1-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateAd1File = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
INS_CO_ID: partsOrder.job.ins_co_nm,
INS_CO_NM: partsOrder.job.ins_co_nm,
INS_ADDR1: partsOrder.job.ins_addr1,
INS_ADDR2: partsOrder.job.ins_addr2,
INS_CITY: partsOrder.job.ins_city,
INS_ST: partsOrder.job.ins_st,
INS_ZIP: partsOrder.job.ins_zip,
INS_CTRY: partsOrder.job.ins_ctry,
INS_PH1: partsOrder.job.ins_ph1,
INS_PH1X: partsOrder.job.ins_ph1x,
INS_PH2: partsOrder.job.ins_ph2,
INS_PH2X: partsOrder.job.ins_ph2x,
INS_FAX: partsOrder.job.ins_fax,
INS_FAXX: partsOrder.job.ins_faxx,
INS_CT_LN: partsOrder.job.ins_ct_ln,
INS_CT_FN: partsOrder.job.ins_ct_fn,
INS_TITLE: partsOrder.job.ins_title,
INS_CT_PH: partsOrder.job.ins_ct_ph,
INS_CT_PHX: partsOrder.job.ins_ct_phx,
INS_EA: partsOrder.job.ins_ea,
INS_MEMO: partsOrder.job.ins_memo,
POLICY_NO: partsOrder.job.policy_no,
DED_AMT: partsOrder.job.ded_amt,
DED_STATUS: partsOrder.job.ded_status,
ASGN_NO: partsOrder.job.asgn_no,
ASGN_DATE: partsOrder.job.asgn_date
? new Date(partsOrder.job.asgn_date)
: null,
ASGN_TYPE: partsOrder.job.asgn_type,
CLM_NO: partsOrder.job.clm_no,
CLM_OFC_ID: partsOrder.job.clm_ofc_id,
CLM_OFC_NM: partsOrder.job.clm_ofc_nm,
CLM_ADDR1: partsOrder.job.clm_addr1,
CLM_ADDR2: partsOrder.job.clm_addr2,
CLM_CITY: partsOrder.job.clm_city,
CLM_ST: partsOrder.job.clm_st,
CLM_ZIP: partsOrder.job.clm_zip,
CLM_CTRY: partsOrder.job.clm_ctry,
CLM_PH1: partsOrder.job.clm_ph1,
CLM_PH1X: partsOrder.job.clm_ph1x,
CLM_PH2: partsOrder.job.clm_ph2,
CLM_PH2X: partsOrder.job.clm_ph2x,
CLM_FAX: partsOrder.job.clm_fax,
CLM_FAXX: partsOrder.job.clm_faxx,
CLM_CT_LN: partsOrder.job.clm_ct_ln,
CLM_CT_FN: partsOrder.job.clm_ct_fn,
CLM_TITLE: partsOrder.job.clm_title,
CLM_CT_PH: partsOrder.job.clm_ct_ph,
CLM_CT_PHX: partsOrder.job.clm_ct_phx,
CLM_EA: partsOrder.job.clm_ea,
PAYEE_NMS: partsOrder.job.payee_nms,
PAY_TYPE: partsOrder.job.pay_type,
PAY_DATE: partsOrder.job.pay_date,
PAY_CHKNM: null, // Explicitly set to null as in original code
PAY_AMT: null, // Explicitly set to null as in original code
PAY_MEMO: partsOrder.job.pay_memo,
AGT_CO_ID: partsOrder.job.agt_co_id,
AGT_CO_NM: partsOrder.job.agt_co_nm,
AGT_ADDR1: partsOrder.job.agt_addr1,
AGT_ADDR2: partsOrder.job.agt_addr2,
AGT_CITY: partsOrder.job.agt_city,
AGT_ST: partsOrder.job.agt_st,
AGT_ZIP: partsOrder.job.agt_zip,
AGT_CTRY: partsOrder.job.agt_ctry,
AGT_PH1: partsOrder.job.agt_ph1,
AGT_PH1X: partsOrder.job.agt_ph1x,
AGT_PH2: partsOrder.job.agt_ph2,
AGT_PH2X: partsOrder.job.agt_ph2x,
AGT_FAX: partsOrder.job.agt_fax,
AGT_FAXX: partsOrder.job.agt_faxx,
AGT_CT_LN: partsOrder.job.agt_ct_ln,
AGT_CT_FN: partsOrder.job.agt_ct_fn,
AGT_CT_PH: partsOrder.job.agt_ct_ph,
AGT_CT_PHX: partsOrder.job.agt_ct_phx,
AGT_EA: partsOrder.job.agt_ea,
AGT_LIC_NO: partsOrder.job.agt_lic_no,
LOSS_DATE: partsOrder.job.loss_date
? new Date(partsOrder.job.loss_date)
: null,
LOSS_CAT: null, // Explicitly set to null as in original code
LOSS_TYPE: null, // Explicitly set to null as in original code
LOSS_DESC: partsOrder.job.loss_desc,
THEFT_IND: null, // Explicitly set to null as in original code
CAT_NO: partsOrder.job.cat_no,
TLOS_IND: null, // Explicitly set to null as in original code
LOSS_MEMO: partsOrder.job.loss_memo,
CUST_PR: partsOrder.job.cust_pr,
INSD_LN: partsOrder.job.insd_ln,
INSD_FN: partsOrder.job.insd_fn,
INSD_TITLE: partsOrder.job.insd_title,
INSD_CO_NM: partsOrder.job.insd_co_nm,
INSD_ADDR1: partsOrder.job.insd_addr1,
INSD_ADDR2: partsOrder.job.insd_addr2,
INSD_CITY: partsOrder.job.insd_city,
INSD_ST: partsOrder.job.insd_st,
INSD_ZIP: partsOrder.job.insd_zip,
INSD_CTRY: partsOrder.job.insd_ctry,
INSD_PH1: partsOrder.job.insd_ph1,
INSD_PH1X: partsOrder.job.insd_ph1x,
INSD_PH2: partsOrder.job.insd_ph2,
INSD_PH2X: partsOrder.job.insd_ph2x,
INSD_FAX: partsOrder.job.insd_fax,
INSD_FAXX: partsOrder.job.insd_faxx,
INSD_EA: partsOrder.job.insd_ea,
OWNR_LN: partsOrder.job.ownr_ln,
OWNR_FN: partsOrder.job.ownr_fn,
OWNR_TITLE: partsOrder.job.ownr_title,
OWNR_CO_NM: partsOrder.job.ownr_co_nm,
OWNR_ADDR1: partsOrder.job.ownr_addr1,
OWNR_ADDR2: partsOrder.job.ownr_addr2,
OWNR_CITY: partsOrder.job.ownr_city,
OWNR_ST: partsOrder.job.ownr_st,
OWNR_ZIP: partsOrder.job.ownr_zip,
OWNR_CTRY: partsOrder.job.ownr_ctry,
OWNR_PH1: partsOrder.job.ownr_ph1,
OWNR_PH1X: partsOrder.job.ownr_ph1x,
OWNR_PH2: partsOrder.job.ownr_ph2,
OWNR_PH2X: partsOrder.job.ownr_ph2x,
OWNR_FAX: partsOrder.job.ownr_fax,
OWNR_FAXX: partsOrder.job.ownr_faxx,
OWNR_EA: partsOrder.job.ownr_ea,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD1`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD1`),
ad1FieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} AD1 file records added.`);
return true;
} catch (error) {
console.error("Error generating AD1 file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateAd1File;

View File

@@ -1,67 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { ad2FieldLineDescriptors } from "../util/ems-interface/fielddescriptors/ad2-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateAd2File = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
EST_CO_NM: partsOrder.job.est_co_nm,
EST_ADDR1: partsOrder.job.est_addr1,
EST_ADDR2: partsOrder.job.est_addr2,
EST_CITY: partsOrder.job.est_city,
EST_ST: partsOrder.job.est_st,
EST_ZIP: partsOrder.job.est_zip,
EST_CTRY: partsOrder.job.est_ctry,
EST_PH1: partsOrder.job.est_ph1,
EST_CT_LN: partsOrder.job.est_ct_ln,
EST_CT_FN: partsOrder.job.est_ct_fn,
EST_EA: partsOrder.job.est_ea,
CLMT_ADDR1: partsOrder.job.clm_addr1,
CLMT_ADDR2: partsOrder.job.clm_addr2,
CLMT_CITY: partsOrder.job.clm_city,
CLMT_ST: partsOrder.job.clm_st,
CLMT_ZIP: partsOrder.job.clm_zip,
CLMT_CTRY: partsOrder.job.clm_ctry,
CLMT_PH1: partsOrder.job.clm_ph1,
CLMT_PH1X: partsOrder.job.clm_ph1x,
CLMT_PH2: partsOrder.job.clm_ph2,
CLMT_PH2X: partsOrder.job.clm_ph2x,
CLMT_FAX: partsOrder.job.clm_fax,
CLMT_FAXX: partsOrder.job.clm_faxx,
CLMT_LN: partsOrder.job.clm_ct_ln,
CLMT_FN: partsOrder.job.clm_ct_fn,
CLMT_TITLE: partsOrder.job.clm_title,
CLMT_CT_PH: partsOrder.job.clm_ct_ph,
CLMT_CT_PHX: partsOrder.job.clm_ct_phx,
CLMT_EA: partsOrder.job.clm_ea,
RF_CO_NM: partsOrder.job.bodyshop.shopname,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD2`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.AD2`),
ad2FieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} AD2 file records added.`);
return true;
} catch (error) {
console.error("Error generating AD2 file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateAd2File;

View File

@@ -1,80 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { envFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/env-field-descriptor";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateEnvFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const dateNow = new Date();
const formatTime = (date: Date): string =>
`${date.getHours().toString().padStart(2, "0")}${date.getMinutes().toString().padStart(2, "0")}${date.getSeconds().toString().padStart(2, "0")}`;
const {
job: { ro_number, ciecaid },
} = partsOrder;
// Find the highest line_ind value
const lineInds = partsOrder.parts_order_lines.map(
(line) => line.jobline.line_ind,
);
const getNumber = (str: string): number => {
const match = str.match(/(\d+)$/);
return match ? parseInt(match[1], 10) : 0;
};
const highestLineInd = lineInds.reduce(
(max, current) => (getNumber(current) > getNumber(max) ? current : max),
lineInds[0] || "",
);
const records = [
{
EST_SYSTEM: "M",
SW_VERSION: "25.3",
DB_VERSION: "OCT_25_V",
DB_DATE: dateNow,
RO_ID: ro_number,
ESTFILE_ID: ciecaid,
SUPP_NO: highestLineInd ? getNumber(highestLineInd).toString() : "1",
EST_CTRY: "CAN",
TOP_SECRET: "00000000-0000-0000-0000-000000000000",
TRANS_TYPE: highestLineInd ? highestLineInd.charAt(0) : "S",
STATUS: false,
CREATE_DT: dateNow,
CREATE_TM: formatTime(dateNow),
TRANSMT_DT: dateNow,
TRANSMT_TM: formatTime(dateNow),
INCL_ADMIN: true,
INCL_VEH: true,
INCL_EST: true,
INCL_PROFL: false,
INCL_TOTAL: false,
INCL_VENDR: false,
EMS_VER: "2.0",
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.ENV`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.ENV`),
envFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} ENV file records added.`);
return true;
} catch (error) {
console.error("Error generating ENV file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateEnvFile;

View File

@@ -1,85 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
import { linFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/lin-field-descriptors";
const EmsPartsOrderGenerateLinFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = partsOrder.parts_order_lines.map((partsOrderLine) => ({
LINE_NO: partsOrderLine.jobline?.line_no,
LINE_IND: partsOrderLine.jobline?.line_ind,
LINE_REF: partsOrderLine.jobline?.line_ref,
TRAN_CODE: partsOrderLine.jobline?.tran_code ?? "1",
DB_REF: partsOrderLine.jobline?.db_ref,
UNQ_SEQ: partsOrderLine.jobline?.unq_seq,
PART_DES_J: false,
LINE_DESC: partsOrderLine.jobline?.line_desc,
PART_TYPE:
partsOrderLine.priceChange === true
? partsOrderLine.part_type
: partsOrderLine.jobline?.part_type,
GLASS_FLAG: partsOrderLine.jobline?.glass_flag,
OEM_PARTNO: partsOrderLine.jobline?.oem_partno,
PRICE_INC: partsOrderLine.jobline?.price_inc,
ALT_PART_I: partsOrderLine.jobline?.alt_part_i,
TAX_PART: partsOrderLine.jobline?.tax_part,
DB_PRICE: partsOrderLine.jobline?.db_price,
ACT_PRICE:
partsOrderLine.priceChange === true
? partsOrderLine.act_price
: partsOrderLine.jobline?.act_price,
PRICE_J: partsOrderLine.jobline?.price_j,
CERT_PART: partsOrderLine.jobline?.cert_part,
PART_QTY: partsOrderLine.jobline?.part_qty,
ALT_CO_ID: partsOrderLine.jobline?.alt_co_id,
ALT_PARTNO: partsOrderLine.jobline?.alt_partno,
ALT_OVERRD: partsOrderLine.jobline?.alt_overrd,
ALT_PARTM: partsOrderLine.jobline?.alt_partm,
PRT_DSMK_P: partsOrderLine.jobline?.prt_dsmk_p,
PRT_DSMK_M: partsOrderLine.jobline?.prt_dsmk_m,
MOD_LBR_TY: partsOrderLine.jobline?.mod_lbr_ty,
DB_HRS: partsOrderLine.jobline?.db_hrs,
MOD_LB_HRS: partsOrderLine.jobline?.mod_lb_hrs,
LBR_INC: partsOrderLine.jobline?.lbr_inc,
LBR_OP: partsOrderLine.jobline?.lbr_op,
LBR_HRS_J: partsOrderLine.jobline?.lbr_hrs_j,
LBR_TYP_J: partsOrderLine.jobline?.lbr_typ_j,
LBR_OP_J: partsOrderLine.jobline?.lbr_op_j,
PAINT_STG: partsOrderLine.jobline?.paint_stg,
PAINT_TONE: partsOrderLine.jobline?.paint_tone,
LBR_TAX: partsOrderLine.jobline?.lbr_tax,
LBR_AMT: partsOrderLine.jobline?.lbr_amt,
MISC_AMT: partsOrderLine.jobline?.misc_amt,
MISC_SUBLT: partsOrderLine.jobline?.misc_sublt,
MISC_TAX: partsOrderLine.jobline?.misc_tax,
BETT_TYPE: partsOrderLine.jobline?.bett_type,
BETT_PCTG: partsOrderLine.jobline?.bett_pctg,
BETT_AMT: partsOrderLine.jobline?.bett_amt,
BETT_TAX: partsOrderLine.jobline?.bett_tax,
}));
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.LIN`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.LIN`),
linFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} LIN file records added.`);
return true;
} catch (error) {
console.error("Error generating LIN file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateLinFile;

View File

@@ -1,59 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
import { pfhFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfh-field-descriptors";
const EmsPartsOrderGeneratePfhFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
ID_PRO_NAM: "REPAIR FACILITY", // Job.id_pro_nam?.Value
TAX_PRETHR: (partsOrder.job.tax_prethr || 0) * 100,
TAX_THRAMT: (partsOrder.job.tax_thramt || 0) * 100,
TAX_PSTTHR: (partsOrder.job.tax_pstthr || 0) * 100,
TAX_TOW_IN: true, // Job.tax_tow_in?.Value
TAX_TOW_RT: (partsOrder.job.tax_tow_rt || 0) * 100,
TAX_STR_IN: true, // Job.tax_str_in?.Value
TAX_STR_RT: (partsOrder.job.tax_str_rt || 0) * 100,
TAX_SUB_IN: true, // Job.tax_sub_in?.Value
TAX_SUB_RT: (partsOrder.job.tax_sub_rt || 0) * 100,
TAX_BTR_IN: true, // Job.tax_btr_in?.Value
TAX_LBR_RT:
(partsOrder.job.bodyshop?.bill_tax_rates?.state_tax_rate || 0) * 100,
TAX_GST_RT:
(partsOrder.job.bodyshop?.bill_tax_rates?.federal_tax_rate || 0) *
100,
TAX_GST_IN: true, // Job.tax_gst_in?.Value
ADJ_G_DISC: (partsOrder.job.adj_g_disc || 0) * 100,
ADJ_TOWDIS: (partsOrder.job.adj_towdis || 0) * 100,
ADJ_STRDIS: (partsOrder.job.adj_strdis || 0) * 100,
ADJ_BTR_IN: null, // Job.adj_btr_in?.Value
TAX_PREDIS: (partsOrder.job.tax_predis || 0) * 100,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFH`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFH`),
pfhFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFH file records added.`);
return true;
} catch (error) {
console.error("Error generating PFH file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfhFile;

View File

@@ -1,302 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPflLine } from "../decoder/decode-pfl.interface";
import { pflFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfl-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
import _ from "lodash";
const EmsPartsOrderGeneratePflFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
let records;
if (partsOrder.job.cieca_pfl && !_.isEmpty(partsOrder.job.cieca_pfl)) {
records = Object.keys(partsOrder.job.cieca_pfl).map((key) => {
const record: DecodedPflLine = partsOrder.job.cieca_pfl[key];
return uppercaseObjectKeys(record);
});
} else {
//We don't have the PFL data for an old job, so make it manually.
records = [
{
LBR_TYPE: "LAA",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_laa,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAB",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lab,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAD",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lad,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAE",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lae,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAF",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_laf,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAG",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lag,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAM",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lam,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAR",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lar,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAS",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_las,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LAU",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_lau,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA1",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la1,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA2",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la2,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA3",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la3,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
{
LBR_TYPE: "LA4",
LBR_DESC: "",
LBR_RATE: partsOrder.job.rate_la4,
LBR_TAX_IN: true,
LBR_TAXP: null, // Job.bodyshop.bill_tax_rates.state_tax_rate?.Value ?? 0,
LBR_ADJP: 0,
LBR_TX_TY1: null, //partsOrder.job.lbr_tx_ty1,
LBR_TX_IN1: null, //partsOrder.job.lbr_tx_in1,
LBR_TX_TY2: null, //partsOrder.job.lbr_tx_ty2,
LBR_TX_IN2: null, //partsOrder.job.lbr_tx_in2,
LBR_TX_TY3: null, //partsOrder.job.lbr_tx_ty3,
LBR_TX_IN3: null, //partsOrder.job.lbr_tx_in3,
LBR_TX_TY4: null, //partsOrder.job.lbr_tx_ty4,
LBR_TX_IN4: null, //partsOrder.job.lbr_tx_in4,
LBR_TX_TY5: null, //partsOrder.job.lbr_tx_ty5,
LBR_TX_IN5: null, //partsOrder.job.lbr_tx_in5,
},
];
}
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFL`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFL`),
pflFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFL file records added.`);
return true;
} catch (error) {
console.error("Error generating PFL file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePflFile;

View File

@@ -1,105 +0,0 @@
import { DBFFile } from "dbffile";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfmLine } from "../decoder/decode-pfm.interface";
import { pfmFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfm-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePfmFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
let records;
if (partsOrder.job.materials && !_.isEmpty(partsOrder.job.materials)) {
records = Object.keys(partsOrder.job.materials).map((key) => {
const record: DecodedPfmLine = partsOrder.job.materials[key];
return uppercaseObjectKeys(record);
});
} else {
//Older records may not have materials, especially for ImEX.
records = [
{
MATL_TYPE: "MAPA",
CAL_CODE: null,
CAL_DESC: null,
CAL_MAXDLR: 0,
CAL_PRIP: 0,
CAL_SECP: 0,
MAT_CALP: 0,
CAL_PRETHR: 0,
CAL_PSTTHR: 0,
CAL_THRAMT: 0,
CAL_LBRMIN: 0,
CAL_LBRMAX: 0,
CAL_LBRRTE: partsOrder.job.rate_mapa,
CAL_OPCODE: null,
TAX_IND: true,
MAT_TAXP: null,
MAT_ADJP: null,
MAT_TX_TY1: null,
MAT_TX_IN1: null,
MAT_TX_TY2: null,
MAT_TX_IN2: null,
MAT_TX_TY3: null,
MAT_TX_IN3: null,
MAT_TX_TY4: null,
MAT_TX_IN4: null,
MAT_TX_TY5: null,
MAT_TX_IN5: null,
},
{
MATL_TYPE: "MASH",
CAL_CODE: null,
CAL_DESC: null,
CAL_MAXDLR: 0,
CAL_PRIP: 0,
CAL_SECP: 0,
MAT_CALP: 0,
CAL_PRETHR: 0,
CAL_PSTTHR: 0,
CAL_THRAMT: 0,
CAL_LBRMIN: 0,
CAL_LBRMAX: 0,
CAL_LBRRTE: partsOrder.job.rate_mash,
CAL_OPCODE: null,
TAX_IND: true,
MAT_TAXP: null,
MAT_ADJP: null,
MAT_TX_TY1: null,
MAT_TX_IN1: null,
MAT_TX_TY2: null,
MAT_TX_IN2: null,
MAT_TX_TY3: null,
MAT_TX_IN3: null,
MAT_TX_TY4: null,
MAT_TX_IN4: null,
MAT_TX_TY5: null,
MAT_TX_IN5: null,
},
];
}
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFM`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFM`),
pfmFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFM file records added.`);
return true;
} catch (error) {
console.error("Error generating PFM file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfmFile;

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { pfoFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfo-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePfoFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = []; //This was kept blank previously as well.
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFO`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFO`),
pfoFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFO file records added.`);
return true;
} catch (error) {
console.error("Error generating PFO file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfoFile;

View File

@@ -1,39 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfpLine } from "../decoder/decode-pfp.interface";
import { pfpFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pfp-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePfpFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = Object.keys(partsOrder.job.parts_tax_rates).map((key) => {
const record: DecodedPfpLine = partsOrder.job.parts_tax_rates[key];
return uppercaseObjectKeys(record);
});
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFP`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFP`),
pfpFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFP file records added.`);
return true;
} catch (error) {
console.error("Error generating PFP file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePfpFile;

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { pftFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/pft-field-descriptor";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGeneratePftFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = []; //Left blank intentionally as per previous code.
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFT`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.PFT`),
pftFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} PFT file records added.`);
return true;
} catch (error) {
console.error("Error generating PFT file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGeneratePftFile;

View File

@@ -1,40 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedStlLine } from "../decoder/decode-stl.interface";
import { stlFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/stl-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateStlFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
//TODO: Add CIECA STL to parts order.
const records = Object.keys(partsOrder.job.cieca_stl?.data).map((key) => {
const record: DecodedStlLine = partsOrder.job.cieca_stl.data[key];
return uppercaseObjectKeys(record);
});
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.STL`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.STL`),
stlFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} STL file records added.`);
return true;
} catch (error) {
console.error("Error generating STL file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateStlFile;

View File

@@ -1,36 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { ttlFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/ttl-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import uppercaseObjectKeys from "../util/uppercaseObjectKeys";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateTtlFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
//TODO: Add CIECA STL to parts order.
const records = uppercaseObjectKeys(partsOrder.job.cieca_ttl?.data);
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.TTL`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.TTL`),
ttlFieldLineDescriptors,
);
await dbf.appendRecords([records]);
console.log(`${records.length} TTL file records added.`);
return true;
} catch (error) {
console.error("Error generating TTL file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateTtlFile;

View File

@@ -1,65 +0,0 @@
import { DBFFile } from "dbffile";
import errorTypeCheck from "../../util/errorTypeCheck";
import { vehFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/veh-field-descriptors";
import {
deleteEmsFileIfExists,
generateEmsOutFilePath,
} from "../util/ems-util";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const EmsPartsOrderGenerateVehFile = async (
partsOrder: EmsPartsOrder,
): Promise<boolean> => {
try {
const records = [
{
IMPACT_1: partsOrder.job.area_of_damage?.impact1 || null,
IMPACT_2: partsOrder.job.area_of_damage?.impact2 || null,
DMG_MEMO: null,
DB_V_CODE: "",
PLATE_NO: partsOrder.job.plate_no || null,
PLATE_ST: partsOrder.job.plate_st || null,
V_VIN: partsOrder.job.v_vin || null,
V_COND: "",
V_PROD_DT: "",
V_MODEL_YR: partsOrder.job.v_model_yr || null,
V_MAKECODE: "",
V_MAKEDESC: partsOrder.job.v_make_desc || null,
V_MODEL: partsOrder.job.v_model_desc || null,
V_TYPE: partsOrder.job.vehicle?.v_type || null,
V_BSTYLE: partsOrder.job.vehicle?.v_bstyle || null,
V_TRIMCODE: partsOrder.job.vehicle?.v_trimcode || null,
TRIM_COLOR: partsOrder.job.vehicle?.trim_color || null,
V_MLDGCODE: partsOrder.job.vehicle?.v_mldgcode || null,
V_ENGINE: partsOrder.job.vehicle?.v_engine || null,
V_MILEAGE: partsOrder.job.vehicle?.v_mileage || null,
V_OPTIONS: null,
V_COLOR: partsOrder.job.vehicle?.v_color || null,
V_TONE: Number(partsOrder.job.vehicle?.v_tone) || null,
V_STAGE: null,
PAINT_CD1: partsOrder.job.vehicle?.v_paint_codes?.paint_cd1 || "",
PAINT_CD2: partsOrder.job.vehicle?.v_paint_codes?.paint_cd2 || "",
PAINT_CD3: partsOrder.job.vehicle?.v_paint_codes?.paint_cd3 || "",
V_MEMO: null,
},
];
await deleteEmsFileIfExists(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.VEH`),
);
const dbf: DBFFile = await DBFFile.create(
generateEmsOutFilePath(`${partsOrder.job.ciecaid}.VEH`),
vehFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} VEH file records added.`);
return true;
} catch (error) {
console.error("Error generating VEH file:", errorTypeCheck(error));
return false;
}
};
export default EmsPartsOrderGenerateVehFile;

View File

@@ -1,83 +0,0 @@
import log from "electron-log/main";
import express from "express";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import createdDirectoryIfNotExist from "../util/createDirectoryIfNotExist";
import EmsPartsOrderGenerateAd1File from "./ems-parts-order-generate-ad1";
import EmsPartsOrderGenerateAd2File from "./ems-parts-order-generate-ad2";
import EmsPartsOrderGenerateEnvFile from "./ems-parts-order-generate-env";
import EmsPartsOrderGenerateLinFile from "./ems-parts-order-generate-lin";
import EmsPartsOrderGeneratePfhFile from "./ems-parts-order-generate-pfh";
import EmsPartsOrderGeneratePflFile from "./ems-parts-order-generate-pfl";
import EmsPartsOrderGeneratePfmFile from "./ems-parts-order-generate-pfm";
import EmsPartsOrderGeneratePfoFile from "./ems-parts-order-generate-pfo";
import EmsPartsOrderGeneratePfpFile from "./ems-parts-order-generate-pfp";
import EmsPartsOrderGeneratePftFile from "./ems-parts-order-generate-pft";
import EmsPartsOrderGenerateStlFile from "./ems-parts-order-generate-stl";
import EmsPartsOrderGenerateTtlFile from "./ems-parts-order-generate-ttl";
import EmsPartsOrderGenerateVehFile from "./ems-parts-order-generate-veh";
import { EmsPartsOrder } from "./ems-parts-order-interfaces";
const handleEMSPartsOrder = async (
req: express.Request,
res: express.Response,
): Promise<void> => {
//Route handler here only.
const partsOrderBody = req.body as EmsPartsOrder;
try {
await generateEMSPartsOrder(partsOrderBody);
res.status(200).json({ success: true });
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
res.status(500).json({
success: false,
error: "Error generating parts price change.",
...errorTypeCheck(error),
});
}
return;
};
const generateEMSPartsOrder = async (
partsOrder: EmsPartsOrder,
): Promise<void> => {
log.debug(" Generating parts price change");
//Check to make sure that the EMS Output file path exists. If it doesn't, create it. If it's not set, abandon ship.
const emsOutFilePath: string | null = store.get("settings.emsOutFilePath");
if (_.isEmpty(emsOutFilePath) || emsOutFilePath === null) {
log.error("EMS Out file path is not set");
throw new Error("EMS Out file path is not set");
}
try {
createdDirectoryIfNotExist(emsOutFilePath);
//Generate all required files: ad1, ad2, veh, lin, pfh, pfl, pfm,pfo, pfp, pft, stl, ttl
await EmsPartsOrderGenerateAd1File(partsOrder);
await EmsPartsOrderGenerateAd2File(partsOrder);
await EmsPartsOrderGenerateVehFile(partsOrder);
await EmsPartsOrderGenerateLinFile(partsOrder);
await EmsPartsOrderGeneratePfhFile(partsOrder);
await EmsPartsOrderGeneratePflFile(partsOrder);
await EmsPartsOrderGeneratePfmFile(partsOrder);
await EmsPartsOrderGeneratePfoFile(partsOrder);
await EmsPartsOrderGeneratePfpFile(partsOrder);
await EmsPartsOrderGeneratePftFile(partsOrder);
await EmsPartsOrderGenerateStlFile(partsOrder);
await EmsPartsOrderGenerateTtlFile(partsOrder);
await EmsPartsOrderGenerateEnvFile(partsOrder);
log.info(
"EMS Parts Order files generated successfully for " +
partsOrder.job.ciecaid,
);
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
throw error;
}
};
export { handleEMSPartsOrder };

View File

@@ -1,322 +0,0 @@
import { CiecaPfl } from "../decoder/decode-pfl.interface";
import { DecodedPfmLine } from "../decoder/decode-pfm.interface";
import { DecodedPfpLine } from "../decoder/decode-pfp.interface";
import { DecodedStlLine } from "../decoder/decode-stl.interface";
import { DecodedTtlLine } from "../decoder/decode-ttl.interface";
export interface TaxRate {
prt_type: string;
prt_discp: number;
prt_mktyp: boolean;
prt_mkupp: number;
prt_tax_in: boolean;
prt_tax_rt: number;
}
export interface BillTaxRates {
local_tax_rate: number;
state_tax_rate: number;
federal_tax_rate: number;
}
export interface PaintCodes {
paint_cd1: string | null;
paint_cd2: string | null;
paint_cd3: string | null;
}
export interface AreaOfDamage {
impact1: string;
impact2: string | null;
}
// Jobline export interface
export interface Jobline {
tran_code: string;
act_price: number;
db_ref: string;
db_price: number;
db_hrs: number;
glass_flag: boolean;
id: string;
lbr_amt: number;
lbr_hrs_j: boolean;
lbr_inc: boolean;
lbr_op: string;
lbr_op_j: boolean;
lbr_tax: boolean;
lbr_typ_j: boolean;
line_desc: string;
line_ind: string;
line_no: number;
line_ref: number;
location: string | null;
misc_amt: number;
misc_sublt: boolean;
misc_tax: boolean;
mod_lb_hrs: number;
mod_lbr_ty: string;
oem_partno: string;
op_code_desc: string;
paint_stg: number;
paint_tone: number;
part_qty: number;
part_type: string;
price_inc: boolean;
price_j: boolean;
prt_dsmk_m: number;
prt_dsmk_p: number;
tax_part: boolean;
unq_seq: number;
alt_co_id: string | null;
alt_overrd: boolean;
alt_part_i: boolean;
alt_partm: string | null;
alt_partno: string | null;
bett_amt: number;
bett_pctg: number;
bett_tax: boolean;
bett_type: string | null;
cert_part: boolean;
est_seq: string | null;
part_descj: boolean;
}
// Parts Order Line export interface
export interface PartsOrderLine {
jobline: Jobline;
act_price: number;
id: string;
db_price: number;
line_desc: string;
quantity: number;
part_type: string;
priceChange: boolean;
}
// Vehicle export interface
export interface Vehicle {
v_bstyle: string;
v_type: string;
v_trimcode: string | null;
v_tone: string;
v_stage: string;
v_prod_dt: string | null;
v_options: string | null;
v_paint_codes: PaintCodes;
v_model_yr: string;
v_model_desc: string;
v_mldgcode: string | null;
v_makecode: string;
v_make_desc: string;
v_engine: string;
v_cond: string;
v_color: string | null;
trim_color: string | null;
shopid: string;
plate_no: string;
plate_st: string;
db_v_code: string;
v_vin: string;
}
// Bodyshop export interface
export interface Bodyshop {
shopname: string;
bill_tax_rates: BillTaxRates;
}
// Job export interface
export interface Job {
bodyshop: Bodyshop;
ro_number: string;
clm_no: string;
asgn_no: string;
asgn_date: string;
state_tax_rate: number | null;
area_of_damage: AreaOfDamage;
asgn_type: string | null;
ciecaid: string;
cieca_pfl: CiecaPfl;
clm_addr1: string | null;
clm_city: string | null;
clm_addr2: string | null;
clm_ct_fn: string | null;
clm_ct_ln: string | null;
clm_ct_ph: string | null;
clm_ct_phx: string | null;
clm_ctry: string | null;
clm_ea: string | null;
clm_fax: string | null;
clm_faxx: string | null;
clm_ofc_id: string | null;
clm_ofc_nm: string | null;
clm_ph1: string | null;
clm_ph1x: string | null;
clm_ph2: string | null;
clm_ph2x: string | null;
clm_st: string | null;
clm_title: string | null;
clm_total: number;
clm_zip: string | null;
ded_amt: number;
est_addr1: string | null;
est_addr2: string | null;
est_city: string | null;
est_co_nm: string | null;
est_ct_fn: string;
est_ctry: string | null;
est_ct_ln: string;
est_ea: string;
est_ph1: string | null;
est_st: string | null;
est_zip: string | null;
g_bett_amt: number;
id: string;
ins_addr1: string | null;
ins_city: string | null;
ins_addr2: string | null;
ins_co_id: string | null;
ins_co_nm: string;
ins_ct_fn: string | null;
ins_ct_ln: string | null;
ins_ct_ph: string | null;
ins_ct_phx: string | null;
ins_ctry: string | null;
ins_ea: string | null;
ins_fax: string | null;
ins_faxx: string | null;
ins_memo: string | null;
ins_ph1: string | null;
ins_ph1x: string | null;
ins_ph2: string | null;
ins_ph2x: string | null;
ins_st: string | null;
ins_title: string | null;
ins_zip: string | null;
insd_addr1: string;
insd_addr2: string | null;
insd_city: string;
insd_co_nm: string | null;
insd_ctry: string | null;
insd_ea: string | null;
insd_fax: string | null;
insd_faxx: string | null;
insd_fn: string;
insd_ln: string;
insd_ph1: string;
insd_ph1x: string | null;
insd_ph2: string;
insd_ph2x: string | null;
insd_st: string;
insd_title: string | null;
insd_zip: string;
loss_cat: string;
loss_date: string;
loss_desc: string;
loss_of_use: string | null;
loss_type: string;
ownr_addr1: string;
ownr_addr2: string | null;
ownr_city: string;
ownr_co_nm: string | null;
ownr_ctry: string | null;
ownr_ea: string | null;
ownr_fax: string | null;
ownr_faxx: string | null;
ownr_ph1: string;
ownr_fn: string;
ownr_ln: string;
ownr_ph1x: string | null;
ownr_ph2: string;
ownr_ph2x: string | null;
ownr_st: string;
ownr_title: string | null;
ownr_zip: string;
parts_tax_rates: Record<string, DecodedPfpLine>;
pay_amt: number;
pay_date: string | null;
pay_type: string | null;
pay_chknm: string;
payee_nms: string | null;
plate_no: string;
plate_st: string;
po_number: string | null;
policy_no: string;
tax_lbr_rt: number;
tax_levies_rt: number;
tax_paint_mat_rt: number;
tax_predis: number;
tax_prethr: number;
tax_pstthr: number;
tax_registration_number: string | null;
tax_str_rt: number;
tax_shop_mat_rt: number;
tax_sub_rt: number;
tax_thramt: number;
tax_tow_rt: number;
theft_ind: boolean;
tlos_ind: boolean;
towin: boolean;
v_color: string | null;
v_make_desc: string;
v_model_desc: string;
v_model_yr: string;
v_vin: string;
vehicle: Vehicle;
agt_zip: string | null;
agt_st: string | null;
agt_ph2x: string | null;
agt_ph2: string | null;
agt_ph1x: string | null;
agt_ph1: string | null;
agt_lic_no: string | null;
agt_faxx: string | null;
agt_fax: string | null;
agt_ea: string | null;
agt_ctry: string | null;
agt_ct_phx: string | null;
agt_ct_ph: string | null;
agt_ct_ln: string | null;
agt_ct_fn: string | null;
agt_co_nm: string | null;
agt_co_id: string | null;
agt_city: string | null;
agt_addr1: string | null;
agt_addr2: string | null;
adj_g_disc: number;
rate_matd: number | null;
rate_mash: number;
rate_mapa: number;
rate_mahw: number;
rate_macs: number;
rate_mabl: number | null;
rate_ma3s: number;
rate_ma2t: number;
rate_ma2s: number;
rate_lau: number;
rate_las: number;
rate_lar: number;
rate_lam: number;
rate_lag: number;
rate_laf: number;
rate_lae: number | null;
rate_lad: number | null;
rate_lab: number;
rate_laa: number;
rate_la4: number;
rate_la3: number;
rate_la2: number;
rate_la1: number;
materials: Record<string, DecodedPfmLine>;
cieca_stl: {
data: Array<DecodedStlLine>;
};
cieca_ttl: { data: DecodedTtlLine };
}
// Main Parts Order export interface
export interface EmsPartsOrder {
parts_order_lines: PartsOrderLine[];
job: Job;
}

View File

@@ -1,210 +0,0 @@
import cors from "cors";
import { app } from "electron";
import log from "electron-log/main";
import express from "express";
import http from "http";
import errorTypeCheck from "../../util/errorTypeCheck";
import ImportJob from "../decoder/decoder";
import folderScan from "../decoder/folder-scan";
import { handleEMSPartsOrder } from "../ems-parts-order/ems-parts-order-handler";
import { handleShopMetaDataFetch } from "../ipc/ipcMainHandler.user";
import { handlePartsPriceChangeRequest } from "../ppc/ppc-handler";
import { handleQuickBookRequest } from "../quickbooks-desktop/quickbooks-desktop";
export default class LocalServer {
private readonly app: express.Application;
private server: http.Server | null;
private PORT = 1337;
constructor() {
this.server = null;
this.app = express();
this.configureMiddleware();
this.configureRoutes();
}
private configureMiddleware(): void {
const allowedOrigins = [
"http://localhost",
"https://localhost",
"http://localhost:3000",
"https://localhost:3000",
"https://test.imex.online",
"https://imex.online",
"https://test.romeonline.io",
"https://romeonline.io",
"https://www.test.imex.online",
"https://www.imex.online",
"https://www.test.romeonline.io",
"https://www.romeonline.io",
];
this.app.use(
cors({
origin: (origin, callback) => {
// Allow requests with no origin (like mobile apps, curl requests)
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
return callback(null, true);
} else {
return callback(null, false);
}
},
credentials: true,
}),
);
// Parse JSON bodies
this.app.use(express.json());
this.app.use(express.urlencoded());
//Add logger Middleware
this.app.use((req, res, next) => {
const startTime = Date.now();
const requestId = Math.random().toString(36).substring(2, 15);
// Log request details
log.info(
`[HTTP Server] [${requestId}] Request: ${req.method} ${req.url}`,
);
log.info(
`[HTTP Server] [${requestId}] Headers: ${JSON.stringify(req.headers)}`,
);
// Log request body if it exists
if (req.body && Object.keys(req.body).length > 0) {
log.info(
`[HTTP Server] [${requestId}] Body: ${JSON.stringify(req.body)}`,
);
}
// Capture the original methods
const originalSend = res.send;
const originalJson = res.json;
// Override send method to log response
res.send = function (body): express.Response {
log.info(`[HTTP Server] [${requestId}] Response body: ${body}`);
log.info(
`[HTTP Server] [${requestId}] Response time: ${Date.now() - startTime}ms`,
);
return originalSend.call(this, body);
};
// Override json method to log response
res.json = function (body): express.Response {
log.info(
`[HTTP Server] [${requestId}] Response body: ${JSON.stringify(body)}`,
);
log.info(
`[HTTP Server] [${requestId}] Response time: ${Date.now() - startTime}ms`,
);
return originalJson.call(this, body);
};
next();
});
}
private configureRoutes(): void {
// Basic health check endpoint
this.app.get("/health", (_req: express.Request, res: express.Response) => {
res.status(200).json({ status: "ok" });
});
this.app.post("/ping", (_req, res) => {
res.status(200).json({
appVer: app.getVersion(),
qbPath: app.getPath("userData"), //TODO: Resolve to actual QB file path.
});
});
this.app.post("/qb", handleQuickBookRequest);
this.app.post("/scan", async (_req, res): Promise<void> => {
log.debug("[HTTP Server] Scan request received");
const files = await folderScan();
res.status(200).json(files);
return;
});
this.app.post("/ppc", handlePartsPriceChangeRequest);
this.app.post("/oec", handleEMSPartsOrder);
this.app.post(
"/import",
async (req: express.Request, res: express.Response) => {
log.debug("[HTTP Server] Import request received");
const { filepath } = req.body;
if (!filepath) {
res.status(400).json({ error: "filepath is required" });
return;
}
try {
await ImportJob(filepath);
res.status(200).json({ success: true });
} catch (error) {
log.error(
"[HTTP Server] Error importing file",
errorTypeCheck(error),
);
res.status(500).json({
success: false,
error: "Error importing file",
...errorTypeCheck(error),
});
}
},
);
this.app.post(
"/refresh",
async (_req: express.Request, res: express.Response) => {
log.debug("[HTTP Server] Refresh request received");
try {
await handleShopMetaDataFetch(true);
res.status(200).json({ success: true });
} catch (error) {
log.error(
"[HTTP Server] Error refreshing shop metadata",
errorTypeCheck(error),
);
res.status(500).json({
success: false,
error: "Error importing file",
...errorTypeCheck(error),
});
}
},
);
// Add more routes as needed
}
public start(): void {
try {
this.server = http.createServer(this.app);
this.server.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "EADDRINUSE") {
log.error(
`[HTTP Server] Port ${this.PORT} is already in use. Please use a different port.`,
);
} else {
log.error(`[HTTP Server] Server error: ${error.message}`);
}
});
this.server.listen(this.PORT, () => {
log.info(
`[HTTP Server] Local HTTP server running on port ${this.PORT}`,
);
});
} catch (error: unknown) {
log.error("[HTTP Server] Error starting server", errorTypeCheck(error));
}
}
public stop(): void {
if (this.server) {
this.server.close();
log.info("[HTTP Server] Local HTTP server stopped");
}
}
}

View File

@@ -14,7 +14,6 @@ import log from "electron-log/main";
import { autoUpdater } from "electron-updater";
import path, { join } from "path";
import imexAppIcon from "../../resources/icon.png?asset";
import romeAppIcon from "../../resources/ro-icon.png?asset";
import {
default as ErrorTypeCheck,
@@ -22,11 +21,8 @@ import {
} from "../util/errorTypeCheck";
import ipcTypes from "../util/ipcTypes.json";
import ImportJob from "./decoder/decoder";
import LocalServer from "./http-server/http-server";
import store from "./store/store";
import { checkForAppUpdates } from "./util/checkForAppUpdates";
import { getMainWindow } from "./util/toRenderer";
import { GetAllEnvFiles } from "./watcher/watcher";
import { dumpMemoryStatsToFile } from "../util/memUsage";
import {
isKeepAliveAgentInstalled,
setupKeepAliveAgent,
@@ -35,11 +31,13 @@ import {
isKeepAliveTaskInstalled,
setupKeepAliveTask,
} from "./setup-keep-alive-task";
import store from "./store/store";
import { checkForAppUpdates } from "./util/checkForAppUpdates";
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
import ongoingMemoryDump, { dumpMemoryStatsToFile } from "../util/memUsage";
import { getMainWindow } from "./util/toRenderer";
import { GetAllEnvFiles } from "./watcher/watcher";
const appIconToUse =
import.meta.env.VITE_COMPANY === "IMEX" ? imexAppIcon : romeAppIcon;
const appIconToUse = imexAppIcon;
Sentry.init({
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
@@ -54,11 +52,10 @@ log.transports.console.format =
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
log.transports.file.maxSize = 50 * 1024 * 1024; // 50 MB
const isMac: boolean = process.platform === "darwin";
const protocol: string = "imexmedia";
const protocol: string = "esdp";
let isAppQuitting = false; //Needed on Mac as an override to allow us to fully quit the app.
let isKeepAliveLaunch = false; // Track if launched via keep-alive
// Initialize the server
const localServer = new LocalServer();
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
@@ -95,7 +92,7 @@ function createWindow(): void {
icon: appIconToUse,
}
: {}),
title: "Shop Partner",
title: "EMS Uploader",
webPreferences: {
preload: join(__dirname, "../preload/index.js"),
sandbox: false,
@@ -425,13 +422,6 @@ function createWindow(): void {
if (!isKeepAliveLaunch) {
mainWindow.show(); // Show only if not a keep-alive launch
}
//Start the HTTP server.
// Start the local HTTP server
try {
localServer.start();
} catch (error) {
log.error("Failed to start HTTP server:", errorTypeCheck(error));
}
});
mainWindow.on("close", (event: Electron.Event) => {
@@ -476,7 +466,7 @@ app.whenReady().then(async () => {
log.debug("App is ready, initializing shortcuts and protocol handlers.");
if (platform.isWindows) {
app.setAppUserModelId("Shop Partner");
app.setAppUserModelId("esdp");
}
app.on("browser-window-created", (_, window) => {
@@ -508,17 +498,8 @@ app.whenReady().then(async () => {
//Dynamically load ipcMain handlers once ready.
try {
const { initializeCronTasks } = await import("./ipc/ipcMainConfig");
await import("./ipc/ipcMainConfig");
log.debug("Successfully loaded ipcMainConfig");
try {
await initializeCronTasks();
log.info("Cron tasks initialized successfully");
} catch (error) {
log.warn("Non-fatal: Failed to initialize cron tasks", {
...ErrorTypeCheck(error),
});
}
} catch (error) {
log.error("Fatal: Failed to load ipcMainConfig", {
...ErrorTypeCheck(error),
@@ -599,7 +580,6 @@ app.whenReady().then(async () => {
//The update itself will run when the bodyshop record is queried to know what release channel to use.
openMainWindow();
ongoingMemoryDump();
app.on("activate", function () {
openMainWindow();
@@ -661,7 +641,6 @@ ipcMain.on(ipcTypes.toMain.updates.apply, () => {
});
function preQuitMethods(): void {
localServer.stop();
const currentSetting = store.get("app.openOnStartup") as boolean;
if (!import.meta.env.DEV) {
app.setLoginItemSettings({

View File

@@ -27,27 +27,6 @@ import {
ipcMainHandleAuthStateChanged,
ipMainHandleResetPassword,
} from "./ipcMainHandler.user";
import cron from "node-cron";
import { PaintScaleConfig, PaintScaleType } from "../../util/types/paintScale";
import { ppgInputHandler, ppgOutputHandler } from "./paintScaleHandlers/PPG";
const initializeCronTasks = async () => {
try {
// Fetch input and output configurations
const inputConfigs = await SettingsPaintScaleInputConfigsGet();
const outputConfigs = await SettingsPaintScaleOutputConfigsGet();
// Start input cron tasks
await handlePaintScaleInputCron(inputConfigs);
log.info("Initialized input cron tasks on app startup");
// Start output cron tasks
await handlePaintScaleOutputCron(outputConfigs);
log.info("Initialized output cron tasks on app startup");
} catch (error) {
log.error("Error initializing cron tasks on app startup:", error);
}
};
// Log all IPC messages and their payloads
const logIpcMessages = (): void => {
@@ -71,75 +50,6 @@ const logIpcMessages = (): void => {
});
};
// Input handler map
const inputTypeHandlers: Partial<
Record<PaintScaleType, (config: PaintScaleConfig) => Promise<void>>
> = {
[PaintScaleType.PPG]: ppgInputHandler,
// Add other input type handlers as needed
};
// Output handler map
const outputTypeHandlers: Partial<
Record<PaintScaleType, (config: PaintScaleConfig) => Promise<void>>
> = {
[PaintScaleType.PPG]: ppgOutputHandler,
// 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}m`,
);
});
};
// 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}m`,
);
});
};
// Existing IPC handlers...
ipcMain.on(ipcTypes.toMain.test, () =>
@@ -230,25 +140,6 @@ ipcMain.handle(
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");
});
@@ -273,6 +164,4 @@ ipcMain.on(ipcTypes.toMain.updates.download, () => {
});
});
export { initializeCronTasks };
logIpcMessages();

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import { envFieldLineDescriptors } from "../util/ems-interface/fielddescriptors/env-field-descriptor";
import { deleteEmsFileIfExists, generatePpcFilePath } from "../util/ems-util";
import { PpcJob } from "./ppc-handler";
const GenerateEnvFile = async (job: PpcJob): Promise<boolean> => {
const records = [
{
EST_SYSTEM: "C",
RO_ID: job.ro_number,
ESTFILE_ID: job.ciecaid,
STATUS: false,
INCL_ADMIN: true,
INCL_VEH: true,
INCL_EST: true,
INCL_PROFL: true,
INCL_TOTAL: true,
INCL_VENDR: false,
},
];
await deleteEmsFileIfExists(generatePpcFilePath(`${job.ciecaid}.ENV`));
const dbf = await DBFFile.create(
generatePpcFilePath(`${job.ciecaid}.ENV`),
envFieldLineDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} LIN file records added.`);
return true;
};
export default GenerateEnvFile;

View File

@@ -1,34 +0,0 @@
import { DBFFile } from "dbffile";
import { linFieldDescriptors } from "../util/ems-interface/fielddescriptors/lin-field-descriptor";
import { deleteEmsFileIfExists, generatePpcFilePath } from "../util/ems-util";
import { PpcJob } from "./ppc-handler";
import errorTypeCheck from "../../util/errorTypeCheck";
const GenerateLinFile = async (job: PpcJob): Promise<boolean> => {
try {
const records = job.joblines.map((line) => {
return {
//TODO: There are missing types here. May require server side updates, but we are missing things like LINE_NO, LINE_IND, etc.
TRAN_CODE: "2",
UNQ_SEQ: line.unq_seq,
ACT_PRICE: line.act_price,
};
});
await deleteEmsFileIfExists(generatePpcFilePath(`${job.ciecaid}.LIN`));
const dbf = await DBFFile.create(
generatePpcFilePath(`${job.ciecaid}.LIN`),
linFieldDescriptors,
);
await dbf.appendRecords(records);
console.log(`${records.length} LIN file records added.`);
return true;
} catch (error) {
console.error("Error generating PPC LIN file", errorTypeCheck(error));
throw error;
}
};
export default GenerateLinFile;

View File

@@ -1,67 +0,0 @@
import { UUID } from "crypto";
import log from "electron-log/main";
import express from "express";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import createdDirectoryIfNotExist from "../util/createDirectoryIfNotExist";
import GenerateEnvFile from "./ppc-generate-env";
import GenerateLinFile from "./ppc-generate-lin";
const handlePartsPriceChangeRequest = async (
req: express.Request,
res: express.Response,
): Promise<void> => {
//Route handler here only.
const job = req.body as PpcJob;
try {
await generatePartsPriceChange(job);
res.status(200).json({ success: true });
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
res.status(500).json({
success: false,
error: "Error generating parts price change.",
...errorTypeCheck(error),
});
}
return;
};
const generatePartsPriceChange = async (job: PpcJob): Promise<void> => {
log.debug(" Generating parts price change");
//Check to make sure that the PPC Output file path exists. If it doesn't, create it. If it's not set, abandon ship.
const ppcOutFilePath: string | null = store.get("settings.ppcFilePath");
if (_.isEmpty(ppcOutFilePath) || ppcOutFilePath === null) {
log.error("PPC file path is not set");
throw new Error("PPC file path is not set");
}
try {
createdDirectoryIfNotExist(ppcOutFilePath);
await GenerateLinFile(job);
await GenerateEnvFile(job);
} catch (error) {
log.error("Error generating parts price change", errorTypeCheck(error));
throw error;
}
};
export interface PpcJob {
id: UUID;
ciecaid: string;
ro_number: string;
joblines: {
removed: boolean;
act_price_before_ppc: number | null;
id: string;
act_price: number;
unq_seq: string; //TODO: Might be a number.
}[];
bodyshop: {
timezone: string;
};
}
export { handlePartsPriceChangeRequest };

View File

@@ -1,30 +0,0 @@
using System;
using Interop.QBFC16; // Ensure this matches your DLL version
public class QuickBooksConnector
{
public string ProcessQBXML(string qbxmlRequest)
{
try
{
QBSessionManager sessionManager = new QBSessionManager();
sessionManager.OpenConnection("", "YourAppName");
sessionManager.BeginSession("", ENOpenMode.omDontCare);
IMsgSetRequest requestMsgSet = sessionManager.CreateMsgSetRequest("US", 13, 0);
requestMsgSet.AppendXML(qbxmlRequest);
IMsgSetResponse responseMsgSet = sessionManager.DoRequests(requestMsgSet);
string qbxmlResponse = responseMsgSet.ToXMLString();
sessionManager.EndSession();
sessionManager.CloseConnection();
return qbxmlResponse;
}
catch (Exception ex)
{
return $"Error: {ex.Message}";
}
}
}

View File

@@ -1,130 +0,0 @@
import log from "electron-log/main";
import { UUID } from "crypto";
import { Request, Response } from "express";
import _ from "lodash";
import errorTypeCheck from "../../util/errorTypeCheck";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let Winax: any; // Declare Winax as any to avoid TypeScript errors on non-Windows platforms
if (process.platform === "win32") {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Winax = require("winax");
}
export async function handleQuickBookRequest(
req: Request,
res: Response,
): Promise<void> {
if (process.platform !== "win32") {
res.status(500).json({
error: "QuickBooks Desktop integration is only available on Windows",
});
return;
}
const QbFilePath: string = `C:\\Users\\PatrickFic\\Development\\FRODO COLLISION.QBW`;
// ||
// (store.get("settings.qbFilePath") as string) F
if (_.isEmpty(QbFilePath)) {
res.status(400).json({ error: "Quickbooks file path not set" });
return;
}
const qbxmlRequestList = req.body as Array<{
id: UUID;
okStatusCodes: Array<string>;
qbxml: string;
}>;
const returnResponse: Array<{
Id: UUID;
Success: boolean;
ErrorMessage: string;
}> = [];
//Connect to the QuickBooks File
let requestProcessor;
try {
requestProcessor = new Winax.Object("QBXMLRP2.RequestProcessor.2");
requestProcessor.OpenConnection(QbFilePath, "ShopPartnerActualRequest");
} catch (error) {
log.error(
"Error instnatiating QuickBooks Request Processor",
QbFilePath,
errorTypeCheck(error),
);
res.status(500).json({ error: "Error connecting to QuickBooks" });
return;
}
const ticket = requestProcessor.BeginSession(QbFilePath, 2); //2 indicated qbFileOpenModeDoNotCare
log.info("Quickbooks Ticket", ticket);
for (const qbxmlRequest of qbxmlRequestList) {
try {
//TODO: Refactor to not create a new connection every time.
const QuickBooksResponse = requestProcessor.ProcessRequest(
ticket,
qbxmlRequest.qbxml,
);
log.info("QuickBooks Raw Response: ", QuickBooksResponse);
returnResponse.push({
Id: qbxmlRequest.id,
Success:
QuickBooksResponse.StatusCode === "0" ||
qbxmlRequest.okStatusCodes.includes(QuickBooksResponse.StatusCode),
ErrorMessage: QuickBooksResponse,
});
} catch (error) {
log.error(
"Error running transaction",
ticket,
qbxmlRequest,
errorTypeCheck(error),
);
}
}
requestProcessor.EndSession(ticket);
requestProcessor.CloseConnection();
res.json(qbxmlRequestList);
}
//This set of functions works.
export function TestQB(): void {
if (process.platform !== "win32") {
log.warn("TestQB is only available on Windows");
return;
}
let requestProcessor, ticket;
try {
requestProcessor = new Winax.Object("QBXMLRP.RequestProcessor.1");
requestProcessor.OpenConnection("", "ShopPartnerOneoFf");
ticket = requestProcessor.BeginSession("", 2); //2 indicated qbFileOOpenModeDoNotCare
requestProcessor.ProcessRequest(
ticket,
`<?qbxml version="16.0"?>
<QBXML>
<QBXMLMsgsRq onError="stopOnError">
<AccountQueryRq requestID="1"> </AccountQueryRq>
</QBXMLMsgsRq>
</QBXML>`,
);
} catch (error) {
log.error(
"Error instnatiating QuickBooks Request Processor",
errorTypeCheck(error),
);
return;
}
log.log("Ticket", ticket);
requestProcessor.EndSession(ticket);
requestProcessor.CloseConnection();
return;
}

View File

@@ -5,11 +5,7 @@ const store = new Store({
settings: {
runOnStartup: true,
filepaths: [],
ppcFilePath: null,
emsOutFilePath: null,
qbFilePath: "",
runWatcherOnStartup: true,
enableMemDebug: false,
polling: {
enabled: false,
interval: 30000,
@@ -25,9 +21,6 @@ const store = new Store({
user: null,
isTest: false,
bodyshop: {},
masterdata: {
opcodes: null,
},
},
},
});

View File

@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>Shop Partner</title>
<title>EMS Uploader</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta
http-equiv="Content-Security-Policy"

View File

@@ -1,97 +0,0 @@
import { test, expect } from "@playwright/test";
import { Page } from "@playwright/test";
// src/renderer/src/App.test.tsx
// Mock data
const mockUser = {
uid: "test123",
email: "test@example.com",
displayName: "Test User",
toJSON: () => ({
uid: "test123",
email: "test@example.com",
displayName: "Test User",
}),
};
test.describe("App Component", () => {
let page: Page;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
// Mock Firebase Auth
await page.addInitScript(() => {
window.mockAuthState = null;
// Mock the firebase auth module
jest.mock("./util/firebase", () => ({
auth: {
onAuthStateChanged: (callback) => {
callback(window.mockAuthState);
// Return mock unsubscribe function
return () => {};
},
},
}));
// Mock electron IPC
window.electron = {
ipcRenderer: {
send: jest.fn(),
},
};
});
await page.goto("/");
});
test("should show SignInForm when user is not authenticated", async () => {
await page.evaluate(() => {
window.mockAuthState = null;
});
await page.reload();
// Check if SignInForm is visible
const signInForm = await page
.locator("form")
.filter({ hasText: "Sign In" });
await expect(signInForm).toBeVisible();
});
test("should show routes when user is authenticated", async () => {
await page.evaluate((user) => {
window.mockAuthState = user;
}, mockUser);
await page.reload();
// Check if AuthHome is visible
const authHome = await page.locator('div:text("AuthHome")');
await expect(authHome).toBeVisible();
// Check that electron IPC was called with auth state
await expect(
page.evaluate(() => {
return window.electron.ipcRenderer.send.mock.calls.length > 0;
}),
).resolves.toBe(true);
});
test("should navigate to settings page when authenticated", async () => {
await page.evaluate((user) => {
window.mockAuthState = user;
}, mockUser);
await page.reload();
// Navigate to settings
await page.click('a[href="/settings"]');
// Check if Settings page is visible
const settingsPage = await page.locator('div:text("Settings")');
await expect(settingsPage).toBeVisible();
});
});

View File

@@ -1,44 +1,16 @@
import "@ant-design/v5-patch-for-react-19";
import { Layout, Skeleton, ConfigProvider, Badge } from "antd";
import { User } from "firebase/auth";
import { useEffect, useState, FC } from "react";
import { Badge, ConfigProvider, Layout, Skeleton } from "antd";
import { FC } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { Provider } from "react-redux";
import { HashRouter, Route, Routes } from "react-router";
import ipcTypes from "../../util/ipcTypes.json";
import ErrorBoundaryFallback from "./components/ErrorBoundaryFallback/ErrorBoundaryFallback";
import Settings from "./components/Settings/Settings";
import SignInForm from "./components/SignInForm/SignInForm";
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
import reduxStore from "./redux/redux-store";
import { auth } from "./util/firebase";
import { NotificationProvider } from "./util/notificationContext";
const App: FC = () => {
const [user, setUser] = useState<User | boolean | null>(false);
useEffect(() => {
// Only set up the listener once when component mounts
if (auth.currentUser) {
setUser(auth.currentUser);
} else {
setUser(false);
}
const unsubscribe = auth.onAuthStateChanged((user: User | null) => {
setUser(user);
//Send back to the main process so that it knows we are authenticated.
if (user) {
window.electron.ipcRenderer.send(
ipcTypes.toMain.authStateChanged,
user.toJSON(),
);
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start);
}
});
// Clean up the listener when component unmounts
return (): void => unsubscribe();
}, []);
const isTest = window.api.isTest();
return (
@@ -58,23 +30,19 @@ const App: FC = () => {
<HashRouter>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<NotificationProvider>
<Skeleton loading={user === false} active>
<Skeleton loading={false} active>
<Layout style={{ minHeight: "100vh" }}>
{!user ? (
<SignInForm />
) : (
<Badge.Ribbon
text={isTest && "Connected to Test"}
color={isTest ? "red" : undefined}
>
<Layout.Content style={{ padding: "0 24px" }}>
<UpdateAvailable />
<Routes>
<Route path="/" element={<Settings />} />
</Routes>
</Layout.Content>
</Badge.Ribbon>
)}
<Badge.Ribbon
text={isTest && "Connected to Test"}
color={isTest ? "red" : undefined}
>
<Layout.Content style={{ padding: "0 24px" }}>
<UpdateAvailable />
<Routes>
<Route path="/" element={<Settings />} />
</Routes>
</Layout.Content>
</Badge.Ribbon>
</Layout>
</Skeleton>
</NotificationProvider>

View File

@@ -1,11 +0,0 @@
import { FC } from "react";
const Home: FC = () => {
return (
<div>
<h1>Home</h1>
</div>
);
};
export default Home;

View File

@@ -1,164 +0,0 @@
import { useState, useEffect } from "react";
import ipcTypes from "../../../../../util/ipcTypes.json";
import {
PaintScaleConfig,
PaintScaleType,
} from "../../../../../util/types/paintScale";
import { message } from "antd";
import { useTranslation } from "react-i18next";
type ConfigType = "input" | "output";
export const usePaintScaleConfig = (configType: ConfigType) => {
const [paintScaleConfigs, setPaintScaleConfigs] = useState<
PaintScaleConfig[]
>([]);
const { t } = useTranslation();
// Get the appropriate IPC methods based on config type
const getConfigsMethod =
configType === "input"
? ipcTypes.toMain.settings.paintScale.getInputConfigs
: ipcTypes.toMain.settings.paintScale.getOutputConfigs;
const setConfigsMethod =
configType === "input"
? ipcTypes.toMain.settings.paintScale.setInputConfigs
: ipcTypes.toMain.settings.paintScale.setOutputConfigs;
const setPathMethod =
configType === "input"
? ipcTypes.toMain.settings.paintScale.setInputPath
: ipcTypes.toMain.settings.paintScale.setOutputPath;
// Load paint scale configs on mount
useEffect(() => {
window.electron.ipcRenderer
.invoke(getConfigsMethod)
.then((configs: PaintScaleConfig[]) => {
// Ensure all configs have a pollingInterval and type (for backward compatibility)
const defaultPolling = configType === "input" ? 1440 : 60;
const updatedConfigs = configs.map((config) => ({
...config,
pollingInterval: config.pollingInterval || defaultPolling, // Default to 1440 for input, 60 for output
type: config.type || PaintScaleType.PPG, // Default type if missing
}));
setPaintScaleConfigs(updatedConfigs || []);
})
.catch((error) => {
console.error(
`Failed to load paint scale ${configType} configs:`,
error,
);
});
}, [getConfigsMethod]);
// Save configs to store and notify main process of config changes
const saveConfigs = (configs: PaintScaleConfig[]) => {
window.electron.ipcRenderer
.invoke(setConfigsMethod, configs)
.then(() => {
// Notify main process to update cron job
if (configType === "input") {
window.electron.ipcRenderer.send(
ipcTypes.toMain.settings.paintScale.updateInputCron,
configs,
);
} else if (configType === "output") {
window.electron.ipcRenderer.send(
ipcTypes.toMain.settings.paintScale.updateOutputCron,
configs,
);
}
})
.catch((error) => {
console.error(
`Failed to save paint scale ${configType} configs:`,
error,
);
});
};
// New helper to check if a path is unique across input and output configs
const checkPathUnique = async (newPath: string): Promise<boolean> => {
try {
const inputConfigs: PaintScaleConfig[] =
await window.electron.ipcRenderer.invoke(
ipcTypes.toMain.settings.paintScale.getInputConfigs,
);
const outputConfigs: PaintScaleConfig[] =
await window.electron.ipcRenderer.invoke(
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
);
const allConfigs = [...inputConfigs, ...outputConfigs];
// Allow updating the current config even if its current value equals newPath.
return !allConfigs.some((config) => config.path === newPath);
} catch (error) {
console.error("Failed to check unique path:", error);
return false;
}
};
// Handle adding a new paint scale config
const handleAddConfig = (type: PaintScaleType) => {
const defaultPolling = configType === "input" ? 1440 : 60;
const newConfig: PaintScaleConfig = {
id: Date.now().toString(),
type,
pollingInterval: defaultPolling, // Default to 1440 for input, 60 for output
};
const updatedConfigs = [...paintScaleConfigs, newConfig];
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
};
// Handle removing a config
const handleRemoveConfig = (id: string) => {
const updatedConfigs = paintScaleConfigs.filter(
(config) => config.id !== id,
);
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
};
// Handle path selection (modified to check directory uniqueness)
const handlePathChange = async (id: string) => {
try {
const path: string | null = await window.electron.ipcRenderer.invoke(
setPathMethod,
id,
);
if (path) {
const isUnique = await checkPathUnique(path);
if (!isUnique) {
message.error(t("settings.errors.duplicatePath"));
return;
}
const updatedConfigs = paintScaleConfigs.map((config) =>
config.id === id ? { ...config, path } : config,
);
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
}
} catch (error) {
console.error(`Failed to set paint scale ${configType} path:`, error);
}
};
// Handle polling interval change
const handlePollingIntervalChange = (id: string, pollingInterval: number) => {
const updatedConfigs = paintScaleConfigs.map((config) =>
config.id === id ? { ...config, pollingInterval } : config,
);
setPaintScaleConfigs(updatedConfigs);
saveConfigs(updatedConfigs);
};
return {
paintScaleConfigs,
handleAddConfig,
handleRemoveConfig,
handlePathChange,
handlePollingIntervalChange,
};
};

View File

@@ -1,46 +0,0 @@
import { FolderOpenFilled } from "@ant-design/icons";
import { Button, Card, Input, Space } from "antd";
import { useEffect, useState, FC } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const SettingsEmsOutFilePath: FC = () => {
const { t } = useTranslation();
const [emsFilePath, setEmsFilePath] = useState<string | null>(null);
const getPollingStateFromStore = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.getEmsOutFilePath)
.then((filePath: string | null) => {
setEmsFilePath(filePath);
});
};
//Get state first time it renders.
useEffect(() => {
getPollingStateFromStore();
}, []);
const handlePathChange = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.setEmsOutFilePath)
.then((filePath: string | null) => {
setEmsFilePath(filePath);
});
};
return (
<Card title={t("settings.labels.emsOutFilePath")}>
<Space wrap>
<Input
value={emsFilePath || ""}
placeholder={t("settings.labels.emsOutFilePath")}
disabled
/>
<Button onClick={handlePathChange} icon={<FolderOpenFilled />} />
</Space>
</Card>
);
};
export default SettingsEmsOutFilePath;

View File

@@ -1,183 +0,0 @@
import {
CheckCircleFilled,
FileAddFilled,
FolderOpenFilled,
WarningFilled,
} from "@ant-design/icons";
import {
Button,
Card,
Input,
Modal,
Select,
Space,
Table,
Tag,
theme,
Tooltip,
} from "antd";
import { JSX, useState } from "react";
import { useTranslation } from "react-i18next";
import {
PaintScaleConfig,
PaintScaleType,
paintScaleTypeOptions,
} from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleInputPaths = (): JSX.Element => {
const { t } = useTranslation();
const { token } = theme.useToken(); // Access theme tokens
const {
paintScaleConfigs,
handleAddConfig,
handleRemoveConfig,
handlePathChange,
handlePollingIntervalChange,
} = usePaintScaleConfig("output");
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
// Show modal when adding a new path
const showAddPathModal = () => {
setSelectedType(null);
setIsModalVisible(true);
};
// Handle modal confirmation
const handleModalOk = () => {
if (selectedType) {
handleAddConfig(selectedType);
setIsModalVisible(false);
}
};
// Handle modal cancellation
const handleModalCancel = () => {
setIsModalVisible(false);
};
// Table columns for paint scale configs
const columns = [
{
title: t("settings.labels.paintScaleType"),
dataIndex: "type",
key: "type",
render: (type: PaintScaleType) => {
const typeOption = paintScaleTypeOptions.find(
(option) => option.value === type,
);
const label = typeOption ? typeOption.label : type;
const colorMap: Partial<Record<PaintScaleType, string>> = {
[PaintScaleType.PPG]: "blue",
// Add other types and colors as needed
};
return <Tag color={colorMap[type] || "default"}>{label}</Tag>;
},
},
{
title: t("settings.labels.paintScalePath"),
dataIndex: "path",
key: "path",
render: (path: string | null, record: PaintScaleConfig) => {
const isValid = path && path.trim() !== "";
return (
<Space>
<Input
value={path || ""}
placeholder={t("settings.labels.paintScalePath")}
disabled
style={{
borderColor: isValid ? token.colorSuccess : token.colorError, // Use semantic tokens
}}
suffix={
<Tooltip
title={
isValid
? t("settings.labels.validPath")
: t("settings.labels.invalidPath")
}
>
{isValid ? (
<CheckCircleFilled style={{ color: token.colorSuccess }} />
) : (
<WarningFilled style={{ color: token.colorError }} />
)}
</Tooltip>
}
/>
<Button
onClick={() => handlePathChange(record.id)}
icon={<FolderOpenFilled />}
/>
</Space>
);
},
},
{
title: t("settings.labels.pollingInterval"),
dataIndex: "pollingInterval",
key: "pollingInterval",
render: (pollingInterval: number, record: PaintScaleConfig) => (
<Input
type="number"
value={pollingInterval}
onChange={(e) =>
handlePollingIntervalChange(record.id, Number(e.target.value))
}
style={{ width: 100 }}
placeholder={t("settings.labels.pollingInterval")}
/>
),
},
{
title: t("settings.labels.actions"),
key: "actions",
render: (_: any, record: PaintScaleConfig) => (
<Button danger onClick={() => handleRemoveConfig(record.id)}>
{t("settings.labels.remove")}
</Button>
),
},
];
return (
<>
<Card
title={t("settings.labels.paintScaleSettingsInput")}
extra={
<Button onClick={showAddPathModal} icon={<FileAddFilled />}>
{t("settings.actions.addpath")}
</Button>
}
>
<Table
dataSource={paintScaleConfigs}
columns={columns}
rowKey="id"
pagination={false}
/>
</Card>
<Modal
title={t("settings.labels.selectPaintScaleType")}
open={isModalVisible}
onOk={handleModalOk}
onCancel={handleModalCancel}
okButtonProps={{ disabled: !selectedType }}
>
<Select
value={selectedType}
options={paintScaleTypeOptions}
onChange={(value) => setSelectedType(value)}
style={{ width: "100%" }}
placeholder={t("settings.labels.selectPaintScaleType")}
/>
</Modal>
</>
);
};
export default SettingsPaintScaleInputPaths;

View File

@@ -1,173 +0,0 @@
import {
CheckCircleFilled,
FileAddFilled,
FolderOpenFilled,
WarningFilled,
} from "@ant-design/icons";
import {
Button,
Card,
Input,
Modal,
Select,
Space,
Table,
Tag,
theme,
} from "antd";
import { JSX, useState } from "react";
import { useTranslation } from "react-i18next";
import {
PaintScaleConfig,
PaintScaleType,
paintScaleTypeOptions,
} from "../../../../util/types/paintScale";
import { usePaintScaleConfig } from "./PaintScale/usePaintScaleConfig";
const SettingsPaintScaleOutputPaths = (): JSX.Element => {
const { token } = theme.useToken();
const { t } = useTranslation();
const {
paintScaleConfigs,
handleAddConfig,
handleRemoveConfig,
handlePathChange,
handlePollingIntervalChange,
} = usePaintScaleConfig("input");
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedType, setSelectedType] = useState<PaintScaleType | null>(null);
// Show modal when adding a new path
const showAddPathModal = () => {
setSelectedType(null);
setIsModalVisible(true);
};
// Handle modal confirmation
const handleModalOk = () => {
if (selectedType) {
handleAddConfig(selectedType);
setIsModalVisible(false);
}
};
// Handle modal cancellation
const handleModalCancel = () => {
setIsModalVisible(false);
};
// Table columns for paint scale configs
const columns = [
{
title: t("settings.labels.paintScaleType"),
dataIndex: "type",
key: "type",
render: (type: PaintScaleType) => {
const typeOption = paintScaleTypeOptions.find(
(option) => option.value === type,
);
const label = typeOption ? typeOption.label : type;
const colorMap: Partial<Record<PaintScaleType, string>> = {
[PaintScaleType.PPG]: "blue",
// Add other types and colors as needed
};
return <Tag color={colorMap[type] || "default"}>{label}</Tag>;
},
},
{
title: t("settings.labels.paintScalePath"),
dataIndex: "path",
key: "path",
render: (path: string | null, record: PaintScaleConfig) => {
const isValid = path && path.trim() !== "";
return (
<Space>
<Input
value={path || ""}
placeholder={t("settings.labels.paintScalePath")}
disabled
style={{
borderColor: isValid ? token.colorSuccess : token.colorError,
}}
suffix={
isValid ? (
<CheckCircleFilled style={{ color: token.colorSuccess }} />
) : (
<WarningFilled style={{ color: token.colorError }} />
)
}
/>
<Button
onClick={() => handlePathChange(record.id)}
icon={<FolderOpenFilled />}
/>
</Space>
);
},
},
{
title: t("settings.labels.pollingInterval"),
dataIndex: "pollingInterval",
key: "pollingInterval",
render: (pollingInterval: number, record: PaintScaleConfig) => (
<Input
type="number"
value={pollingInterval}
onChange={(e) =>
handlePollingIntervalChange(record.id, Number(e.target.value))
}
style={{ width: 100 }}
placeholder={t("settings.labels.pollingInterval")}
/>
),
},
{
title: t("settings.labels.actions"),
key: "actions",
render: (_: any, record: PaintScaleConfig) => (
<Button danger onClick={() => handleRemoveConfig(record.id)}>
{t("settings.labels.remove")}
</Button>
),
},
];
return (
<>
<Card
title={t("settings.labels.paintScaleSettingsOutput")}
extra={
<Button onClick={showAddPathModal} icon={<FileAddFilled />}>
{t("settings.actions.addpath")}
</Button>
}
>
<Table
dataSource={paintScaleConfigs}
columns={columns}
rowKey="id"
pagination={false}
/>
</Card>
<Modal
title={t("settings.labels.selectPaintScaleType")}
open={isModalVisible}
onOk={handleModalOk}
onCancel={handleModalCancel}
okButtonProps={{ disabled: !selectedType }}
>
<Select
value={selectedType}
options={paintScaleTypeOptions}
onChange={(value) => setSelectedType(value)}
style={{ width: "100%" }}
placeholder={t("settings.labels.selectPaintScaleType")}
/>
</Modal>
</>
);
};
export default SettingsPaintScaleOutputPaths;

View File

@@ -1,46 +0,0 @@
import { FolderOpenFilled } from "@ant-design/icons";
import { Button, Card, Input, Space } from "antd";
import { useEffect, useState, FC } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const SettingsPpcFilepath: FC = () => {
const { t } = useTranslation();
const [ppcFilePath, setPpcFilePath] = useState<string | null>(null);
const getPollingStateFromStore = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.getPpcFilePath)
.then((filePath: string | null) => {
setPpcFilePath(filePath);
});
};
//Get state first time it renders.
useEffect(() => {
getPollingStateFromStore();
}, []);
const handlePathChange = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.setPpcFilePath)
.then((filePath: string | null) => {
setPpcFilePath(filePath);
});
};
return (
<Card title={t("settings.labels.ppcfilepath")}>
<Space wrap>
<Input
value={ppcFilePath || ""}
placeholder={t("settings.labels.ppcfilepath")}
disabled
/>
<Button onClick={handlePathChange} icon={<FolderOpenFilled />} />
</Space>
</Card>
);
};
export default SettingsPpcFilepath;

View File

@@ -3,43 +3,23 @@ import { Col, Row } from "antd";
import { FC } from "react";
import SettingsWatchedPaths from "./Settings.WatchedPaths";
import SettingsWatcher from "./Settings.Watcher";
import Welcome from "../Welcome/Welcome";
import SettingsPpcFilepath from "./Settings.PpcFilePath";
import SettingsEmsOutFilePath from "./Settings.EmsOutFilePath";
import SettingsPaintScaleInputPaths from "./Settings.PaintScaleInputPaths";
import SettingsPaintScaleOutputPaths from "./Settings.PaintScaleOutputPaths";
const colSpans = {
md: 12, // Two columns on medium screens and above
sm: 24, // One column on small screens
md: 12, // Two columns on medium screens and above
sm: 24, // One column on small screens
};
const Settings: FC = () => {
return (
<Row gutter={[16, 16]}>
<Col span={24}>
<Welcome />
</Col>
<Col {...colSpans}>
<SettingsWatchedPaths />
</Col>
<Col {...colSpans}>
<SettingsWatcher />
</Col>
<Col {...colSpans}>
<SettingsPpcFilepath />
</Col>
<Col {...colSpans}>
<SettingsEmsOutFilePath />
</Col>
<Col {...colSpans}>
<SettingsPaintScaleInputPaths />
</Col>
<Col {...colSpans}>
<SettingsPaintScaleOutputPaths />
</Col>
</Row>
);
return (
<Row gutter={[16, 16]}>
<Col {...colSpans}>
<SettingsWatchedPaths />
</Col>
<Col {...colSpans}>
<SettingsWatcher />
</Col>
</Row>
);
};
export default Settings;
export default Settings;

View File

@@ -1,142 +0,0 @@
import { auth } from "@renderer/util/firebase";
import type { FormProps } from "antd";
import { Alert, Button, Card, Form, Input, Typography } from "antd";
import log from "electron-log/renderer";
import { signInWithEmailAndPassword } from "firebase/auth";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import errorTypeCheck from "../../../../util/errorTypeCheck";
import ipcTypes from "../../../../util/ipcTypes.json";
const { Title } = Typography;
type FieldType = {
username: string;
password: string;
remember?: string;
};
const SignInForm: React.FC = () => {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const { t } = useTranslation();
const onFinish: FormProps<FieldType>["onFinish"] = async (values) => {
const { username, password } = values;
setLoading(true);
try {
const result = await signInWithEmailAndPassword(auth, username, password);
log.debug("Login result", result);
} catch (error) {
log.error("Login error", errorTypeCheck(error));
setError(t("auth.login.error"));
} finally {
setLoading(false);
}
};
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
errorInfo,
) => {
log.log("Failed:", errorInfo);
};
return (
<Card
style={{
maxWidth: 600,
margin: "auto auto",
borderRadius: 8,
paddingLeft: 48,
paddingRight: 48,
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
}}
>
<div style={{ textAlign: "center", marginBottom: 24 }}>
<Title level={2}>
{import.meta.env.VITE_COMPANY === "IMEX"
? t("title.imex")
: t("title.rome")}
</Title>
</div>
<Form
name="desktop-sign-in"
layout="vertical"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
requiredMark={false}
>
{error && (
<Form.Item>
<Alert
message={error}
type="error"
showIcon
style={{ marginBottom: 16 }}
/>
</Form.Item>
)}
<Form.Item<FieldType>
label="Username"
name="username"
rules={[
{
required: true,
message: t(
"auth.login.usernameRequired",
"Please enter your username",
),
},
]}
>
<Input size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password"
name="password"
rules={[
{
required: true,
message: t(
"auth.login.passwordRequired",
"Please enter your password",
),
},
]}
>
<Input.Password size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
loading={loading}
htmlType="submit"
size="large"
block
>
{t("auth.login.login")}
</Button>
</Form.Item>
<Form.Item style={{ marginBottom: 0, textAlign: "center" }}>
<Button
type="link"
onClick={(): void => {
window.electron.ipcRenderer.send(
ipcTypes.toMain.user.resetPassword,
);
}}
>
{t("auth.login.resetpassword")}
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default SignInForm;

View File

@@ -1,15 +0,0 @@
import { JSX, useState } from "react";
function Versions(): JSX.Element {
const [versions] = useState(window.electron.process.versions);
return (
<ul className="versions">
<li className="electron-version">Electron v{versions.electron}</li>
<li className="chrome-version">Chromium v{versions.chrome}</li>
<li className="node-version">Node v{versions.node}</li>
</ul>
);
}
export default Versions;

View File

@@ -1,49 +0,0 @@
import { LogoutOutlined } from "@ant-design/icons";
import { auth } from "@renderer/util/firebase";
import { Button, Space, Typography } from "antd";
import { isEmpty } from "lodash";
import { JSX, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const Welcome = (): JSX.Element => {
const { t } = useTranslation();
const [shopName, setShopName] = useState<string | null>(null);
useEffect(() => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.user.getActiveShop)
.then((shopName: string) => {
console.log("Active shop name:", shopName);
setShopName(shopName);
});
}, []);
return (
<>
<Typography.Title level={4}>
{t("auth.labels.welcome", {
name: isEmpty(auth.currentUser?.displayName)
? auth.currentUser?.email
: `${auth.currentUser?.displayName} (${auth.currentUser?.email})`.trim(),
})}
</Typography.Title>
<Space align="baseline">
<Typography.Paragraph>{shopName || ""}</Typography.Paragraph>
<Button
size="small"
danger
icon={<LogoutOutlined />}
onClick={(): void => {
auth.signOut().catch((error) => {
console.error("Sign out error:", error);
});
}}
>
{t("navigation.signout")}
</Button>
</Space>
</>
);
};
export default Welcome;

View File

@@ -1,17 +0,0 @@
export enum PaintScaleType {
PPG = "PPG",
}
export interface PaintScaleConfig {
id: string;
path?: string;
type: PaintScaleType;
pollingInterval: number;
}
export const paintScaleTypeOptions = Object.values(PaintScaleType).map(
(type) => ({
value: type,
label: type,
}),
);