Initial copy of shop partner app.

This commit is contained in:
Patrick Fic
2025-12-01 05:43:59 -08:00
commit 267ef714a7
193 changed files with 32199 additions and 0 deletions

12
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
/// <reference types="vite/client" />
export interface ImportMetaEnv {
readonly VITE_FIREBASE_CONFIG: string;
readonly VITE_GRAPHQL_ENDPOINT: string;
readonly VITE_FIREBASE_CONFIG_TEST: string;
readonly VITE_GRAPHQL_ENDPOINT_TEST: string;
}
export interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -0,0 +1,149 @@
import { UUID } from "crypto";
export interface DecodedAd1 {
// Insurance company information
ins_co_id?: string;
ins_co_nm?: string;
ins_addr1?: string;
ins_addr2?: string;
ins_city?: string;
ins_st?: string;
ins_zip?: string;
ins_ctry?: string;
ins_ea?: string;
ins_ph1?: string;
ins_ph1x?: string;
ins_ph2?: string;
ins_ph2x?: string;
ins_fax?: string;
ins_faxx?: string;
ins_ct_ln?: string;
ins_ct_fn?: string;
ins_title?: string;
ins_ct_ph?: string;
ins_ct_phx?: string;
// Policy information
policy_no?: string;
ded_amt?: string;
ded_status?: string;
asgn_no?: string;
asgn_date?: Date | string;
asgn_type?: string;
// Claim information
clm_no?: string;
clm_ofc_id?: string;
clm_ofc_nm?: string;
clm_addr1?: string;
clm_addr2?: string;
clm_city?: string;
clm_st?: string;
clm_zip?: string;
clm_ctry?: string;
clm_ph1?: string;
clm_ph1x?: string;
clm_ph2?: string;
clm_ph2x?: string;
clm_fax?: string;
clm_faxx?: string;
clm_ct_ln?: string;
clm_ct_fn?: string;
clm_title?: string;
clm_ct_ph?: string;
clm_ct_phx?: string;
clm_ea?: string;
// Payment information
payee_nms?: string;
pay_type?: string;
pay_date?: string;
pay_chknm?: string;
pay_amt?: string;
// Agent information
agt_co_id?: string;
agt_co_nm?: string;
agt_addr1?: string;
agt_addr2?: string;
agt_city?: string;
agt_st?: string;
agt_zip?: string;
agt_ctry?: string;
agt_ph1?: string;
agt_ph1x?: string;
agt_ph2?: string;
agt_ph2x?: string;
agt_fax?: string;
agt_faxx?: string;
agt_ct_ln?: string;
agt_ct_fn?: string;
agt_ct_ph?: string;
agt_ct_phx?: string;
agt_ea?: string;
agt_lic_no?: string;
// Loss information
loss_date?: string;
loss_type?: string;
loss_desc?: string;
theft_ind?: string;
cat_no?: string;
tlos_ind?: string;
cust_pr?: string;
loss_cat?: string;
// Insured information
insd_ln?: string;
insd_fn?: string;
insd_title?: string;
insd_co_nm?: string;
insd_addr1?: string;
insd_addr2?: string;
insd_city?: string;
insd_st?: string;
insd_zip?: string;
insd_ctry?: string;
insd_ph1?: string;
insd_ph2?: string;
insd_fax?: string;
insd_faxx?: string;
insd_ea?: string;
// Owner information
ownr_ln?: string;
ownr_fn?: string;
ownr_title?: string;
ownr_co_nm?: string;
ownr_addr1?: string;
ownr_addr2?: string;
ownr_city?: string;
ownr_st?: string;
ownr_zip?: string;
ownr_ctry?: string;
ownr_ph1?: string;
ownr_ph2?: string;
ownr_ea?: string;
// Owner data object - referenced in the code
owner: {
data: OwnerRecordInterface;
};
}
export interface OwnerRecordInterface {
ownr_ln?: string;
ownr_fn?: string;
ownr_title?: string;
ownr_co_nm?: string;
ownr_addr1?: string;
ownr_addr2?: string;
ownr_city?: string;
ownr_st?: string;
ownr_zip?: string;
ownr_ctry?: string;
ownr_ph1?: string;
ownr_ph2?: string;
ownr_ea?: string;
shopid: UUID;
}

View File

@@ -0,0 +1,237 @@
import { platform } from "@electron-toolkit/utils";
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import { DecodedAd1, OwnerRecordInterface } from "./decode-ad1.interface";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeAD1 = async (
extensionlessFilePath: string,
): Promise<DecodedAd1> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}A.AD1`);
} catch {
// log.debug("Error opening AD1 File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.AD1`);
// log.debug("Trying to find AD1 file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any AD1 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD1 files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["a.ad1", ".ad1"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any AD1 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD1 files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening AD1 File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD1 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawAd1Data: DecodedAd1 = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"INS_CO_ID",
"INS_CO_NM",
"INS_ADDR1",
"INS_ADDR2",
"INS_CITY",
"INS_ST",
"INS_ZIP",
"INS_CTRY",
"INS_EA",
"POLICY_NO",
"DED_AMT",
"DED_STATUS",
"ASGN_NO",
"ASGN_DATE",
"ASGN_TYPE",
"CLM_NO",
"CLM_OFC_ID",
"CLM_OFC_NM",
"CLM_ADDR1",
"CLM_ADDR2",
"CLM_CITY",
"CLM_ST",
"CLM_ZIP",
"CLM_CTRY",
"CLM_PH1",
"CLM_PH1X",
"CLM_PH2",
"CLM_PH2X",
"CLM_FAX",
"CLM_FAXX",
"CLM_CT_LN",
"CLM_CT_FN",
"CLM_TITLE",
"CLM_CT_PH",
"CLM_CT_PHX",
"CLM_EA",
"PAYEE_NMS",
"PAY_TYPE",
"PAY_DATE",
"PAY_CHKNM",
"PAY_AMT",
"AGT_CO_ID",
"AGT_CO_NM",
"AGT_ADDR1",
"AGT_ADDR2",
"AGT_CITY",
"AGT_ST",
"AGT_ZIP",
"AGT_CTRY",
"AGT_PH1",
"AGT_PH1X",
"AGT_PH2",
"AGT_PH2X",
"AGT_FAX",
"AGT_FAXX",
"AGT_CT_LN",
"AGT_CT_FN",
"AGT_CT_PH",
"AGT_CT_PHX",
"AGT_EA",
"AGT_LIC_NO",
"LOSS_DATE",
"LOSS_TYPE",
"LOSS_DESC",
"THEFT_IND",
"CAT_NO",
"TLOS_IND",
"CUST_PR",
"INSD_LN",
"INSD_FN",
"INSD_TITLE",
"INSD_CO_NM",
"INSD_ADDR1",
"INSD_ADDR2",
"INSD_CITY",
"INSD_ST",
"INSD_ZIP",
"INSD_CTRY",
"INSD_PH1",
//"INSD_PH1X",
"INSD_PH2",
//"INSD_PH2X",
"INSD_FAX",
"INSD_FAXX",
"INSD_EA",
"OWNR_LN",
"OWNR_FN",
"OWNR_TITLE",
"OWNR_CO_NM",
"OWNR_ADDR1",
"OWNR_ADDR2",
"OWNR_CITY",
"OWNR_ST",
"OWNR_ZIP",
"OWNR_CTRY",
"OWNR_PH1",
//"OWNR_PH1X",
"OWNR_PH2",
//"OWNR_PH2X",
//"OWNR_FAX",
//"OWNR_FAXX",
"OWNR_EA",
"INS_PH1",
"INS_PH1X",
"INS_PH2",
"INS_PH2X",
"INS_FAX",
"INS_FAXX",
"INS_CT_LN",
"INS_CT_FN",
"INS_TITLE",
"INS_CT_PH",
"INS_CT_PHX",
"LOSS_CAT",
]),
);
//Copy specific logic for manipulation.
//If ownr_ph1 is missing, use ownr_ph2
if (rawAd1Data.asgn_date) {
const newAsgnDate = new Date(rawAd1Data.asgn_date);
rawAd1Data.asgn_date = newAsgnDate.toISOString().split("T")[0];
}
if (!rawAd1Data.ownr_ph1 || _.isEmpty(rawAd1Data.ownr_ph1)) {
rawAd1Data.ownr_ph1 = rawAd1Data.ownr_ph2;
}
if (rawAd1Data.clm_no === "") {
rawAd1Data.clm_no = undefined;
}
let ownerRecord: OwnerRecordInterface;
//Check if the owner information is there. If not, use the insured information as a fallback.
if (
_.isEmpty(rawAd1Data.ownr_ln) &&
_.isEmpty(rawAd1Data.ownr_fn) &&
_.isEmpty(rawAd1Data.ownr_co_nm)
) {
//They're all empty. Using the insured information as a fallback.
// Build up the owner record to insert it alongside the job.
//TODO: Verify that this should be the insured, and not the claimant.
ownerRecord = {
ownr_ln: rawAd1Data.insd_ln,
ownr_fn: rawAd1Data.insd_fn,
ownr_title: rawAd1Data.insd_title,
ownr_co_nm: rawAd1Data.insd_co_nm,
ownr_addr1: rawAd1Data.insd_addr1,
ownr_addr2: rawAd1Data.insd_addr2,
ownr_city: rawAd1Data.insd_city,
ownr_st: rawAd1Data.insd_st,
ownr_zip: rawAd1Data.insd_zip,
ownr_ctry: rawAd1Data.insd_ctry,
ownr_ph1: rawAd1Data.insd_ph1,
ownr_ph2: rawAd1Data.insd_ph2,
ownr_ea: rawAd1Data.insd_ea,
shopid: store.get("app.bodyshop.id"),
};
} else {
//Use the owner information.
ownerRecord = {
ownr_ln: rawAd1Data.ownr_ln,
ownr_fn: rawAd1Data.ownr_fn,
ownr_title: rawAd1Data.ownr_title,
ownr_co_nm: rawAd1Data.ownr_co_nm,
ownr_addr1: rawAd1Data.ownr_addr1,
ownr_addr2: rawAd1Data.ownr_addr2,
ownr_city: rawAd1Data.ownr_city,
ownr_st: rawAd1Data.ownr_st,
ownr_zip: rawAd1Data.ownr_zip,
ownr_ctry: rawAd1Data.ownr_ctry,
ownr_ph1: rawAd1Data.ownr_ph1,
ownr_ph2: rawAd1Data.ownr_ph2,
ownr_ea: rawAd1Data.ownr_ea,
shopid: store.get("app.bodyshop.id"),
};
}
return { ...rawAd1Data, owner: { data: ownerRecord } };
};
export default DecodeAD1;

View File

@@ -0,0 +1,29 @@
export interface DecodedAD2 {
clmt_ln?: string;
clmt_fn?: string;
clmt_title?: string;
clmt_co_nm?: string;
clmt_addr1?: string;
clmt_addr2?: string;
clmt_city?: string;
clmt_st?: string;
clmt_zip?: string;
clmt_ctry?: string;
clmt_ph1?: string;
clmt_ph2?: string;
clmt_ea?: string;
est_co_id?: string;
est_co_nm?: string;
est_addr1?: string;
est_addr2?: string;
est_city?: string;
est_st?: string;
est_zip?: string;
est_ctry?: string;
est_ph1?: string;
est_ct_ln?: string;
est_ct_fn?: string;
est_ea?: string;
date_estimated?: Date; // This is transformed from insp_date
insp_date?: Date; // This exists initially but gets deleted
}

View File

@@ -0,0 +1,170 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import { DecodedAD2 } from "./decode-ad2.interface";
import { platform } from "@electron-toolkit/utils";
import errorTypeCheck from "../../util/errorTypeCheck";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeAD2 = async (
extensionlessFilePath: string,
): Promise<DecodedAD2> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}B.AD2`);
} catch {
dbf = await DBFFile.open(`${extensionlessFilePath}.AD2`);
}
if (!dbf) {
log.error(`Could not find any AD2 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD2 files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["b.ad2", ".ad2"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any AD2 files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any AD2 files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening AD2 File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawAd2Data: DecodedAD2 = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"CLMT_LN", //TODO: This claimant info shouldnt be passed back. Just for the owner info.
"CLMT_FN",
"CLMT_TITLE",
"CLMT_CO_NM",
"CLMT_ADDR1",
"CLMT_ADDR2",
"CLMT_CITY",
"CLMT_ST",
"CLMT_ZIP",
"CLMT_CTRY",
"CLMT_PH1",
//"CLMT_PH1X",
"CLMT_PH2",
//"CLMT_PH2X",
//"CLMT_FAX",
//"CLMT_FAXX",
"CLMT_EA",
//"EST_CO_ID",
"EST_CO_NM",
"EST_ADDR1",
"EST_ADDR2",
"EST_CITY",
"EST_ST",
"EST_ZIP",
"EST_CTRY",
"EST_PH1",
//"EST_PH1X",
//"EST_PH2",
//"EST_PH2X",
//"EST_FAX",
//"EST_FAXX",
"EST_CT_LN",
"EST_CT_FN",
"EST_EA",
//"EST_LIC_NO",
//"EST_FILENO",
//"INSP_CT_LN",
//"INSP_CT_FN",
//"INSP_ADDR1",
//"INSP_ADDR2",
//"INSP_CITY",
//"INSP_ST",
//"INSP_ZIP",
//"INSP_CTRY",
//"INSP_PH1",
//"INSP_PH1X",
//"INSP_PH2",
//"INSP_PH2X",
//"INSP_FAX",
//"INSP_FAXX",
//"INSP_EA",
//"INSP_CODE",
//"INSP_DESC",
"INSP_DATE", //RENAME TO date_estimated
//"INSP_TIME",
//"RF_CO_ID",
//"RF_CO_NM",
//"RF_ADDR1",
//"RF_ADDR2",
//"RF_CITY",
//"RF_ST",
//"RF_ZIP",
//"RF_CTRY",
//"RF_PH1",
//"RF_PH1X",
//"RF_PH2",
//"RF_PH2X",
//"RF_FAX",
//"RF_FAXX",
//"RF_CT_LN",
//"RF_CT_FN",
//"RF_EA",
//"RF_TAX_ID",
//"RF_LIC_NO",
//"RF_BAR_NO",
//"RO_IN_DATE",
//"RO_IN_TIME",
//"TAR_DATE",
//"TAR_TIME",
//"RO_CMPDATE",
//"RO_CMPTIME",
//"DATE_OUT",
//"TIME_OUT",
//"RF_ESTIMTR",
//"MKTG_TYPE",
//"MKTG_SRC",
//"LOC_NM",
//"LOC_ADDR1",
//"LOC_ADDR2",
//"LOC_CITY",
//"LOC_ST",
//"LOC_ZIP",
//"LOC_CTRY",
//"LOC_PH1",
//"LOC_PH1X",
//"LOC_PH2",
//"LOC_PH2X",
//"LOC_FAX",
//"LOC_FAXX",
//"LOC_CT_LN",
//"LOC_CT_FN",
//"LOC_TITLE",
//"LOC_PH",
//"LOC_PHX",
//"LOC_EA",
]),
);
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
rawAd2Data.date_estimated = rawAd2Data.insp_date;
delete rawAd2Data.insp_date;
return rawAd2Data;
};
export default DecodeAD2;

View File

@@ -0,0 +1,5 @@
export interface DecodedEnv {
est_system?: string;
estfile_id?: string;
ciecaid?: string;
}

View File

@@ -0,0 +1,68 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedEnv } from "./decode-env.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeEnv = async (
extensionlessFilePath: string,
): Promise<DecodedEnv> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.ENV`);
} catch (error) {
log.error("Error opening ENV File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any ENV files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any ENV files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".env"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any ENV files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any ENV files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening ENV File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//TODO: Determine if there's any value to capture the whole ENV file.
const rawEnvData: DecodedEnv = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
//TODO: Several of these fields will fail. Should extend schema to capture them.
//"EST_SYSTEM",
"ESTFILE_ID",
]),
);
rawEnvData.ciecaid = rawEnvData.estfile_id;
delete rawEnvData.estfile_id;
//Apply business logic transfomrations.
return rawEnvData;
};
export default DecodeEnv;

View File

@@ -0,0 +1,54 @@
export interface DecodedLinLine {
line_no?: string;
line_ind?: string;
line_ref?: string;
tran_code?: string;
db_ref?: string;
unq_seq?: string;
//who_pays?: string;
line_desc?: string;
part_type?: string;
//part_desc_j?: boolean;
glass_flag?: boolean;
oem_partno?: string;
price_inc?: boolean;
alt_part_i?: boolean;
tax_part?: boolean;
db_price?: number;
act_price?: number;
price_j?: boolean;
cert_part?: boolean;
part_qty?: number;
alt_co_id?: string;
alt_partno?: string;
alt_overrd?: boolean;
alt_partm?: string;
prt_dsmk_p?: string;
prt_dsmk_m?: string;
mod_lbr_ty?: string;
db_hrs?: number;
mod_lb_hrs?: number;
lbr_inc?: boolean;
lbr_op?: string;
lbr_hrs_j?: boolean;
lbr_typ_j?: boolean;
lbr_op_j?: boolean;
paint_stg?: string;
paint_tone?: string;
lbr_tax?: boolean;
lbr_amt?: number;
misc_amt?: number;
misc_sublt?: string;
misc_tax?: boolean;
bett_type?: string;
bett_pctg?: string | number;
bett_amt?: number;
bett_tax?: boolean;
op_code_desc?: string;
}
export interface DecodedLin {
joblines: {
data: DecodedLinLine[];
};
}

View File

@@ -0,0 +1,120 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import { DecodedLin, DecodedLinLine } from "./decode-lin.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeLin = async (
extensionlessFilePath: string,
): Promise<DecodedLin> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.LIN`);
} catch (error) {
//LIN File only has 1 location.
log.error("Error opening LIN File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any LIN files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any LIN files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["lin"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any LIN files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any LIN files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening LIN File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const opCodeData = store.get("app.masterdata.opcodes"); //TODO: Type the op codes
const rawLinData: DecodedLinLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedLinLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"LINE_NO",
"LINE_IND",
"LINE_REF",
"TRAN_CODE",
"DB_REF",
"UNQ_SEQ",
// "WHO_PAYS",
"LINE_DESC",
"PART_TYPE",
//TODO: Believe this was previously broken in partner. Need to confirm.
// system == "M" ? "PART_DESCJ" : "PART_DES_J",
//"PART_DESC_J",
//End Check
"GLASS_FLAG",
"OEM_PARTNO",
"PRICE_INC",
"ALT_PART_I",
"TAX_PART",
"DB_PRICE",
"ACT_PRICE",
"PRICE_J",
"CERT_PART",
"PART_QTY",
"ALT_CO_ID",
"ALT_PARTNO",
"ALT_OVERRD",
"ALT_PARTM",
"PRT_DSMK_P",
"PRT_DSMK_M",
"MOD_LBR_TY",
"DB_HRS",
"MOD_LB_HRS",
"LBR_INC",
"LBR_OP",
"LBR_HRS_J",
"LBR_TYP_J",
"LBR_OP_J",
"PAINT_STG",
"PAINT_TONE",
"LBR_TAX",
"LBR_AMT",
"MISC_AMT",
"MISC_SUBLT",
"MISC_TAX",
"BETT_TYPE",
"BETT_PCTG",
"BETT_AMT",
"BETT_TAX",
]),
);
//Apply line by line adjustments.
singleLineData.op_code_desc = opCodeData[singleLineData.lbr_op]?.desc;
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { joblines: { data: rawLinData } };
};
export default DecodeLin;

View File

@@ -0,0 +1,15 @@
export interface DecodedPfh {
tax_prethr: number;
tax_thr_amt?: number;
tax_pstthr?: number;
tax_tow_rt: number;
tax_str_rt: number;
tax_sub_rt: number;
tax_lbr_rt: number;
federal_tax_rate: number;
adj_g_disc?: number;
adj_towdis?: number;
adj_strdis?: number;
tax_predis?: number;
tax_gst_rt?: number;
}

View File

@@ -0,0 +1,94 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPfh } from "./decode-pfh.interface";
import { platform } from "os";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfh = async (
extensionlessFilePath: string,
): Promise<DecodedPfh> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.PFH`);
log.log("Trying to find PFH file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any PFH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFH files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfh"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFH files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfhData: DecodedPfh = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
//TODO: Several of these fields will fail. Should extend schema to capture them.
//"ID_PRO_NAM", //Remove
"TAX_PRETHR",
"TAX_THRAMT",
"TAX_PSTTHR",
//"TAX_TOW_IN", //Remove
"TAX_TOW_RT",
//"TAX_STR_IN", //Remove
"TAX_STR_RT",
//"TAX_SUB_IN", //Remove
"TAX_SUB_RT",
//"TAX_BTR_IN", //Remove
"TAX_LBR_RT",
"TAX_GST_RT",
//"TAX_GST_IN", //Remove
"ADJ_G_DISC",
"ADJ_TOWDIS",
"ADJ_STRDIS",
//"ADJ_BTR_IN", //Remove
"TAX_PREDIS",
]),
);
//Apply business logic transfomrations.
//Standardize some of the numbers and divide by 100.
rawPfhData.tax_prethr = (rawPfhData.tax_prethr ?? 0) / 100;
rawPfhData.tax_pstthr = (rawPfhData.tax_pstthr ?? 0) / 100;
rawPfhData.tax_tow_rt = (rawPfhData.tax_tow_rt ?? 0) / 100;
rawPfhData.tax_str_rt = (rawPfhData.tax_str_rt ?? 0) / 100;
rawPfhData.tax_sub_rt = (rawPfhData.tax_sub_rt ?? 0) / 100;
rawPfhData.tax_lbr_rt = (rawPfhData.tax_lbr_rt ?? 0) / 100;
rawPfhData.federal_tax_rate = (rawPfhData.tax_gst_rt ?? 0) / 100;
delete rawPfhData.tax_gst_rt;
return rawPfhData;
};
export default DecodePfh;

View File

@@ -0,0 +1,57 @@
//TODO: Clean up this interface. A bit messy as we store the data in very different ways.
export interface DecodedPflLine {
lbr_type: string;
lbr_desc: string;
lbr_rate: number;
lbr_tax_in: boolean;
lbr_taxp: number;
lbr_adjP: number;
lbr_tx_ty1: string;
lbr_tx_in1: boolean;
lbr_tx_ty2: string;
lbr_tx_in2: boolean;
lbr_tx_ty3: string;
lbr_tx_in3: boolean;
lbr_tx_ty4: string;
lbr_tx_in4: boolean;
lbr_tx_ty5: string;
lbr_tx_in5: boolean;
}
export interface JobLaborRateFields {
rate_laa: number;
rate_lab: number;
rate_lad: number;
rate_las: number;
rate_lar: number;
rate_lae: number;
rate_lag: number;
rate_laf: number;
rate_lam: number;
rate_lau: number;
rate_la1: number;
rate_la2: number;
rate_la3: number;
rate_la4: number;
}
export interface CiecaPfl {
LAA?: DecodedPflLine;
LAB?: DecodedPflLine;
LAD?: DecodedPflLine;
LAS?: DecodedPflLine;
LAR?: DecodedPflLine;
LAE?: DecodedPflLine;
LAG?: DecodedPflLine;
LAF?: DecodedPflLine;
LAM?: DecodedPflLine;
LAU?: DecodedPflLine;
LA1?: DecodedPflLine;
LA2?: DecodedPflLine;
LA3?: DecodedPflLine;
LA4?: DecodedPflLine;
}
export interface DecodedPfl extends JobLaborRateFields {
cieca_pfl: CiecaPfl;
}

View File

@@ -0,0 +1,122 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import {
DecodedPfl,
JobLaborRateFields,
DecodedPflLine,
} from "./decode-pfl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfl = async (
extensionlessFilePath: string,
): Promise<DecodedPfl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFL`);
} catch (error) {
//PFL File only has 1 location.
log.error("Error opening PFL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const jobLaborRates: JobLaborRateFields = {
rate_laa: 0,
rate_lab: 0,
rate_lad: 0,
rate_las: 0,
rate_lar: 0,
rate_lae: 0,
rate_lag: 0,
rate_laf: 0,
rate_lam: 0,
rate_lau: 0,
rate_la1: 0,
rate_la2: 0,
rate_la3: 0,
rate_la4: 0,
};
const rawPflData: DecodedPflLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPflLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"LBR_TYPE",
"LBR_DESC",
"LBR_RATE",
"LBR_TAX_IN",
"LBR_TAXP",
"LBR_ADJP",
"LBR_TX_TY1",
"LBR_TX_IN1",
"LBR_TX_TY2",
"LBR_TX_IN2",
"LBR_TX_TY3",
"LBR_TX_IN3",
"LBR_TX_TY4",
"LBR_TX_IN4",
"LBR_TX_TY5",
"LBR_TX_IN5",
]),
);
//Apply line by line adjustments.
//Set the job.rate_<CIECA_TYPE> field based on the value.
jobLaborRates[`rate_${singleLineData.lbr_type.toLowerCase()}`] =
singleLineData.lbr_rate;
//For Mitchell, Alum is stored under LA3 instead of LAA. Shift it back over.
//The old partner had a check for this, but it always was true. Matching that logic.
if (singleLineData.lbr_type === "LA3") {
jobLaborRates[`rate_laa`] = singleLineData.lbr_rate;
}
//Also capture the whole object.
//This is segmented because the whole object was not previously captured for ImEX as it wasn't needed.
//Rome needs the whole object to accurately calculate the tax rates.
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
const pflObj = _.keyBy(rawPflData, "lbr_type");
return { ...jobLaborRates, cieca_pfl: pflObj };
};
export default DecodePfl;

View File

@@ -0,0 +1,50 @@
export interface DecodedPfmLine {
matl_type?: string;
cal_code?: number;
cal_desc?: string;
cal_maxdlr?: number;
cal_prip?: number;
cal_secp?: number;
mat_calp?: number;
cal_prethr?: number;
cal_pstthr?: number;
cal_thramt?: number;
cal_lbrmin?: number;
cal_lbrrte?: number;
cal_opcode?: string;
tax_ind?: boolean;
mat_taxp?: number;
mat_adjp?: number;
mat_tx_ty1?: string;
mat_tx_in1?: boolean;
mat_tx_ty2?: string;
mat_tx_in2?: boolean;
mat_tx_ty3?: string;
mat_tx_in3?: boolean;
mat_tx_ty4?: string;
mat_tx_in4?: boolean;
mat_tx_ty5?: string;
mat_tx_in5?: boolean;
}
export interface JobMaterialRateFields {
rate_mapa: number;
tax_paint_mat_rt: number;
rate_mash: number;
tax_shop_mat_rt: number;
rate_mahw: number;
tax_levies_rt: number;
rate_ma2s: number;
rate_ma2t: number;
rate_ma3s: number;
rate_macs: number;
rate_mabl: number;
}
export interface DecodedPfm extends JobMaterialRateFields {
materials: {
MAPA?: DecodedPfmLine;
MASH?: DecodedPfmLine;
};
cieca_pfm?: DecodedPfmLine[];
}

View File

@@ -0,0 +1,169 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import {
DecodedPfm,
DecodedPfmLine,
JobMaterialRateFields,
} from "./decode-pfm.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfm = async (
extensionlessFilePath: string,
): Promise<DecodedPfm> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFM`);
} catch (error) {
//PFM File only has 1 location.
log.error("Error opening PFM File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFM files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFM files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfm"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFM files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFM files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFM File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const jobMaterialRates: JobMaterialRateFields = {
rate_mapa: 0,
tax_paint_mat_rt: 0,
rate_mash: 0,
tax_shop_mat_rt: 0,
rate_mahw: 0,
tax_levies_rt: 0,
rate_ma2s: 0,
rate_ma2t: 0,
rate_ma3s: 0,
rate_macs: 0,
rate_mabl: 0,
};
const rawPfmData: DecodedPfmLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfmLine = YNBoolConverter(
deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"MATL_TYPE",
"CAL_CODE",
"CAL_DESC",
"CAL_MAXDLR",
"CAL_PRIP",
"CAL_SECP",
"MAT_CALP",
"CAL_PRETHR", //Mitchell here
"CAL_PSTTHR",
"CAL_THRAMT",
"CAL_LBRMIN",
"CAL_LBRRTE", //Audatex puts it here
"CAL_OPCODE",
"TAX_IND",
"MAT_TAXP",
"MAT_ADJP",
"MAT_TX_TY1",
"MAT_TX_IN1",
"MAT_TX_TY2",
"MAT_TX_IN2",
"MAT_TX_TY3",
"MAT_TX_IN3",
"MAT_TX_TY4",
"MAT_TX_IN4",
"MAT_TX_TY5",
"MAT_TX_IN5",
]),
),
);
//Also capture the whole object.
//This is segmented because the whole object was not previously captured for ImEX as it wasn't needed.
//Rome needs the whole object to accurately calculate the tax rates.
return singleLineData;
});
//Apply line by line adjustments.
const mapaLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAPA",
);
if (mapaLine) {
jobMaterialRates.rate_mapa =
mapaLine.cal_lbrrte || mapaLine.cal_prethr || 0;
jobMaterialRates.tax_paint_mat_rt = (mapaLine.mat_taxp ?? 0) / 100;
}
const mashLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MASH",
);
if (mashLine) {
jobMaterialRates.rate_mash =
mashLine.cal_lbrrte || mashLine.cal_prethr || 0;
jobMaterialRates.tax_shop_mat_rt = (mashLine.mat_taxp ?? 0) / 100;
}
const mahwLine: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === "MAHW",
);
if (mahwLine) {
jobMaterialRates.rate_mahw =
mahwLine.cal_lbrrte || mahwLine.cal_prethr || 0;
jobMaterialRates.tax_levies_rt = (mahwLine.mat_taxp ?? 0) / 100;
}
const additionalMaterials = ["MA2S", "MA2T", "MA3S", "MACS", "MABL"];
additionalMaterials.forEach((type) => {
const line: DecodedPfmLine | undefined = rawPfmData.find(
(line) => line.matl_type === type,
);
if (line) {
jobMaterialRates[`rate_${type.toLowerCase()}`] =
line.cal_lbrrte || line.cal_prethr || 0;
}
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return {
...jobMaterialRates,
materials: {
MASH: mashLine,
MAPA: mapaLine, //TODO: Need to verify if more fields are to come in here.
},
//cieca_pfm: rawPfmData, //TODO: Not currently captured. This may have valu in the future.
};
};
export default DecodePfm;

View File

@@ -0,0 +1,32 @@
export interface DecodedPfoLine {
tx_tow_ty?: string;
tow_t_ty1?: string;
tow_t_in1?: boolean;
tow_t_ty2?: string;
tow_t_in2?: boolean;
tow_t_ty3?: string;
tow_t_in3?: boolean;
tow_t_ty4?: string;
tow_t_in4?: boolean;
tow_t_ty5?: string;
tow_t_in5?: boolean;
tow_t_ty6?: string;
tow_t_in6?: boolean;
tx_stor_ty?: string;
stor_t_ty1?: string;
stor_t_in1?: boolean;
stor_t_ty2?: string;
stor_t_in2?: boolean;
stor_t_ty3?: string;
stor_t_in3?: boolean;
stor_t_ty4?: string;
stor_t_in4?: boolean;
stor_t_ty5?: string;
stor_t_in5?: boolean;
stor_t_ty6?: string;
stor_t_in6?: boolean;
}
export interface DecodedPfo {
cieca_pfo: DecodedPfoLine;
}

View File

@@ -0,0 +1,93 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import { DecodedPfo, DecodedPfoLine } from "./decode-pfo.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfo = async (
extensionlessFilePath: string,
): Promise<DecodedPfo> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFO`);
} catch (error) {
log.error("Error opening PFO File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.PFO`);
log.log("Trying to find PFO file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any PFO files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFO files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfo"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFO files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFO files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFO File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFO will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfoData: DecodedPfoLine = YNBoolConverter(
deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TX_TOW_TY",
"TOW_T_TY1",
"TOW_T_IN1",
"TOW_T_TY2",
"TOW_T_IN2",
"TOW_T_TY3",
"TOW_T_IN3",
"TOW_T_TY4",
"TOW_T_IN4",
"TOW_T_TY5",
"TOW_T_IN5",
"TOW_T_TY6",
"TOW_T_IN6",
"TX_STOR_TY",
"STOR_T_TY1",
"STOR_T_IN1",
"STOR_T_TY2",
"STOR_T_IN2",
"STOR_T_TY3",
"STOR_T_IN3",
"STOR_T_TY4",
"STOR_T_IN4",
"STOR_T_TY5",
"STOR_T_IN5",
"STOR_T_TY6",
"STOR_T_IN6",
]),
),
);
//Apply business logic transfomrations.
return { cieca_pfo: rawPfoData };
};
export default DecodePfo;

View File

@@ -0,0 +1,37 @@
export interface DecodedPfpLine {
prt_type: string;
prt_tax_in: boolean;
prt_tax_rt: number;
prt_mkupp: number;
prt_mktyp: string;
prt_discp: number;
prt_tx_ty1: string;
prt_tx_in1: boolean;
prt_tx_ty2: string;
prt_tx_in2: boolean;
prt_tx_ty3: string;
prt_tx_in3: boolean;
prt_tx_ty4: string;
prt_tx_in4: boolean;
prt_tx_ty5: string;
prt_tx_in5: boolean;
}
export interface DecodedPfpLinesByType {
PAA: DecodedPfpLine;
PAC: DecodedPfpLine;
PAL: DecodedPfpLine;
PAG: DecodedPfpLine;
PAM: DecodedPfpLine;
PAP: DecodedPfpLine;
PAN: DecodedPfpLine;
PAO: DecodedPfpLine;
PAR: DecodedPfpLine;
PAS: DecodedPfpLine;
PASL: DecodedPfpLine;
PAT: DecodedPfpLine;
}
export interface DecodedPfp {
parts_tax_rates: DecodedPfpLinesByType;
}

View File

@@ -0,0 +1,98 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import YNBoolConverter from "../../util/ynBoolConverter";
import {
DecodedPfp,
DecodedPfpLine,
DecodedPfpLinesByType,
} from "./decode-pfp.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePfp = async (
extensionlessFilePath: string,
): Promise<DecodedPfp> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFP`);
} catch (error) {
//PFP File only has 1 location.
log.error("Error opening PFP File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFP files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFP files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = [".pfp"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFP files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFP files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFP File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPfpData: DecodedPfpLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedPfpLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"PRT_TYPE",
"PRT_TAX_IN",
"PRT_TAX_RT",
"PRT_MKUPP",
"PRT_MKTYP",
"PRT_DISCP",
"PRT_TX_TY1",
"PRT_TX_IN1",
"PRT_TX_TY2",
"PRT_TX_IN2",
"PRT_TX_TY3",
"PRT_TX_IN3",
"PRT_TX_TY4",
"PRT_TX_IN4",
"PRT_TX_TY5",
"PRT_TX_IN5",
]),
);
singleLineData.prt_tax_rt = singleLineData.prt_tax_rt / 100;
return YNBoolConverter(singleLineData);
});
//Apply business logic transfomrations.
//Convert array of lines to a hash object.
const parsedPfpFile: DecodedPfpLinesByType = rawPfpData.reduce(
(acc: DecodedPfpLinesByType, line: DecodedPfpLine) => {
acc[line.prt_type] = line;
return acc;
},
{} as DecodedPfpLinesByType,
);
return { parts_tax_rates: parsedPfpFile };
};
export default DecodePfp;

View File

@@ -0,0 +1,147 @@
/**
* Interface representing decoded data from a PFT file
* Contains tax type information with up to 6 tax types and 5 tiers each
*/
export interface DecodedPftLine {
// Tax Type 1
tax_type1?: string;
ty1_tier1?: number;
ty1_thres1?: number;
ty1_rate1?: number;
ty1_sur1?: number;
ty1_tier2?: number;
ty1_thres2?: number;
ty1_rate2?: number;
ty1_sur2?: number;
ty1_tier3?: number;
ty1_thres3?: number;
ty1_rate3?: number;
ty1_sur3?: number;
ty1_tier4?: number;
ty1_thres4?: number;
ty1_rate4?: number;
ty1_sur4?: number;
ty1_tier5?: number;
ty1_thres5?: number;
ty1_rate5?: number;
ty1_sur5?: number;
// Tax Type 2
tax_type2?: string;
ty2_tier1?: number;
ty2_thres1?: number;
ty2_rate1?: number;
ty2_sur1?: number;
ty2_tier2?: number;
ty2_thres2?: number;
ty2_rate2?: number;
ty2_sur2?: number;
ty2_tier3?: number;
ty2_thres3?: number;
ty2_rate3?: number;
ty2_sur3?: number;
ty2_tier4?: number;
ty2_thres4?: number;
ty2_rate4?: number;
ty2_sur4?: number;
ty2_tier5?: number;
ty2_thres5?: number;
ty2_rate5?: number;
ty2_sur5?: number;
// Tax Type 3
tax_type3?: string;
ty3_tier1?: number;
ty3_thres1?: number;
ty3_rate1?: number;
ty3_sur1?: number;
ty3_tier2?: number;
ty3_thres2?: number;
ty3_rate2?: number;
ty3_sur2?: number;
ty3_tier3?: number;
ty3_thres3?: number;
ty3_rate3?: number;
ty3_sur3?: number;
ty3_tier4?: number;
ty3_thres4?: number;
ty3_rate4?: number;
ty3_sur4?: number;
ty3_tier5?: number;
ty3_thres5?: number;
ty3_rate5?: number;
ty3_sur5?: number;
// Tax Type 4
tax_type4?: string;
ty4_tier1?: number;
ty4_thres1?: number;
ty4_rate1?: number;
ty4_sur1?: number;
ty4_tier2?: number;
ty4_thres2?: number;
ty4_rate2?: number;
ty4_sur2?: number;
ty4_tier3?: number;
ty4_thres3?: number;
ty4_rate3?: number;
ty4_sur3?: number;
ty4_tier4?: number;
ty4_thres4?: number;
ty4_rate4?: number;
ty4_sur4?: number;
ty4_tier5?: number;
ty4_thres5?: number;
ty4_rate5?: number;
ty4_sur5?: number;
// Tax Type 5
tax_type5?: string;
ty5_tier1?: number;
ty5_thres1?: number;
ty5_rate1?: number;
ty5_sur1?: number;
ty5_tier2?: number;
ty5_thres2?: number;
ty5_rate2?: number;
ty5_sur2?: number;
ty5_tier3?: number;
ty5_thres3?: number;
ty5_rate3?: number;
ty5_sur3?: number;
ty5_tier4?: number;
ty5_thres4?: number;
ty5_rate4?: number;
ty5_sur4?: number;
ty5_tier5?: number;
ty5_thres5?: number;
ty5_rate5?: number;
ty5_sur5?: number;
// Tax Type 6
tax_type6?: string;
ty6_tier1?: number;
ty6_thres1?: number;
ty6_rate1?: number;
ty6_sur1?: number;
ty6_tier2?: number;
ty6_thres2?: number;
ty6_rate2?: number;
ty6_sur2?: number;
ty6_tier3?: number;
ty6_thres3?: number;
ty6_rate3?: number;
ty6_sur3?: number;
ty6_tier4?: number;
ty6_thres4?: number;
ty6_rate4?: number;
ty6_sur4?: number;
ty6_tier5?: number;
ty6_thres5?: number;
ty6_rate5?: number;
ty6_sur5?: number;
}
export interface DecodedPft {
cieca_pft: DecodedPftLine;
}

View File

@@ -0,0 +1,189 @@
import { platform } from "@electron-toolkit/utils";
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedPft, DecodedPftLine } from "./decode-pft.interface";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodePft = async (
extensionlessFilePath: string,
): Promise<DecodedPft> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.PFT`);
} catch (error) {
log.error("Error opening PFH File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any PFT files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFT files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["pft"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any PFT files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any PFT files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening PFT File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFT will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawPftData: DecodedPftLine = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"TAX_TYPE1", //The below is is taken from a CCC estimate. Will require validation to ensure it is also accurate for Audatex/Mitchell
"TY1_TIER1",
"TY1_THRES1",
"TY1_RATE1",
"TY1_SUR1",
"TY1_TIER2",
"TY1_THRES2",
"TY1_RATE2",
"TY1_SUR2",
"TY1_TIER3",
"TY1_THRES3",
"TY1_RATE3",
"TY1_SUR3",
"TY1_TIER4",
"TY1_THRES4",
"TY1_RATE4",
"TY1_SUR4",
"TY1_TIER5",
"TY1_THRES5",
"TY1_RATE5",
"TY1_SUR5",
"TAX_TYPE2",
"TY2_TIER1",
"TY2_THRES1",
"TY2_RATE1",
"TY2_SUR1",
"TY2_TIER2",
"TY2_THRES2",
"TY2_RATE2",
"TY2_SUR2",
"TY2_TIER3",
"TY2_THRES3",
"TY2_RATE3",
"TY2_SUR3",
"TY2_TIER4",
"TY2_THRES4",
"TY2_RATE4",
"TY2_SUR4",
"TY2_TIER5",
"TY2_THRES5",
"TY2_RATE5",
"TY2_SUR5",
"TAX_TYPE3",
"TY3_TIER1",
"TY3_THRES1",
"TY3_RATE1",
"TY3_SUR1",
"TY3_TIER2",
"TY3_THRES2",
"TY3_RATE2",
"TY3_SUR2",
"TY3_TIER3",
"TY3_THRES3",
"TY3_RATE3",
"TY3_SUR3",
"TY3_TIER4",
"TY3_THRES4",
"TY3_RATE4",
"TY3_SUR4",
"TY3_TIER5",
"TY3_THRES5",
"TY3_RATE5",
"TY3_SUR5",
"TAX_TYPE4",
"TY4_TIER1",
"TY4_THRES1",
"TY4_RATE1",
"TY4_SUR1",
"TY4_TIER2",
"TY4_THRES2",
"TY4_RATE2",
"TY4_SUR2",
"TY4_TIER3",
"TY4_THRES3",
"TY4_RATE3",
"TY4_SUR3",
"TY4_TIER4",
"TY4_THRES4",
"TY4_RATE4",
"TY4_SUR4",
"TY4_TIER5",
"TY4_THRES5",
"TY4_RATE5",
"TY4_SUR5",
"TAX_TYPE5",
"TY5_TIER1",
"TY5_THRES1",
"TY5_RATE1",
"TY5_SUR1",
"TY5_TIER2",
"TY5_THRES2",
"TY5_RATE2",
"TY5_SUR2",
"TY5_TIER3",
"TY5_THRES3",
"TY5_RATE3",
"TY5_SUR3",
"TY5_TIER4",
"TY5_THRES4",
"TY5_RATE4",
"TY5_SUR4",
"TY5_TIER5",
"TY5_THRES5",
"TY5_RATE5",
"TY5_SUR5",
"TAX_TYPE6",
"TY6_TIER1",
"TY6_THRES1",
"TY6_RATE1",
"TY6_SUR1",
"TY6_TIER2",
"TY6_THRES2",
"TY6_RATE2",
"TY6_SUR2",
"TY6_TIER3",
"TY6_THRES3",
"TY6_RATE3",
"TY6_SUR3",
"TY6_TIER4",
"TY6_THRES4",
"TY6_RATE4",
"TY6_SUR4",
"TY6_TIER5",
"TY6_THRES5",
"TY6_RATE5",
"TY6_SUR5",
]),
);
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { cieca_pft: rawPftData };
};
export default DecodePft;

View File

@@ -0,0 +1,23 @@
export interface DecodedStlLine {
ttl_type?: string;
ttl_typecd?: string;
t_amt?: number;
t_hrs?: number;
t_addlbr?: number;
t_discamt?: number;
t_mkupamt?: number;
t_gdiscamt?: number;
tax_amt?: number;
nt_amt?: number;
nt_hrs?: number;
nt_addlbr?: number;
nt_disc?: number;
nt_mkup?: number;
nt_gdis?: number;
ttl_typamt?: number;
ttl_hrs?: number;
ttl_amt?: number;
}
export interface DecodedStl {
cieca_stl: { data: DecodedStlLine[] };
}

View File

@@ -0,0 +1,85 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedStl, DecodedStlLine } from "./decode-stl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeStl = async (
extensionlessFilePath: string,
): Promise<DecodedStl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.STL`);
} catch (error) {
log.error("Error opening STL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any STL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any STL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["stl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any STL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any STL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening STL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords();
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawStlData: DecodedStlLine[] = rawDBFRecord.map((record) => {
const singleLineData: DecodedStlLine = deepLowerCaseKeys(
_.pick(record, [
//TODO: Add typings for EMS File Formats.
"TTL_TYPE",
"TTL_TYPECD",
"T_AMT",
"T_HRS",
"T_ADDLBR",
"T_DISCAMT",
"T_MKUPAMT",
"T_GDISCAMT",
"TAX_AMT",
"NT_AMT",
"NT_HRS",
"NT_ADDLBR",
"NT_DISC",
"NT_MKUP",
"NT_GDIS",
"TTL_TYPAMT",
"TTL_HRS",
"TTL_AMT",
]),
);
//Apply line by line adjustments.
return singleLineData;
});
//Apply business logic transfomrations.
//We don't have an inspection date, we instead have `date_estimated`
return { cieca_stl: { data: rawStlData } };
};
export default DecodeStl;

View File

@@ -0,0 +1,22 @@
export interface DecodedTtl {
clm_total: number;
depreciation_taxes: number;
cieca_ttl: { data: DecodedTtlLine };
}
export interface DecodedTtlLine {
g_ttl_amt?: number;
g_bett_amt?: number;
g_rpd_amt?: number;
g_ded_amt?: number;
g_cust_amt?: number;
g_aa_amt?: number;
n_ttl_amt?: number;
prev_net?: number;
supp_amt?: number;
n_supp_amt?: number;
g_upd_amt?: number;
g_ttl_disc?: number;
g_tax?: number;
gst_amt?: number;
}

View File

@@ -0,0 +1,80 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedTtl, DecodedTtlLine } from "./decode-ttl.interface";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeTtl = async (
extensionlessFilePath: string,
): Promise<DecodedTtl> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}.TTL`);
} catch (error) {
log.error("Error opening TTL File.", errorTypeCheck(error));
}
if (!dbf) {
log.error(`Could not find any TTL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any TTL files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["ttl"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any TTL files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any TTL files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath);
} catch (error) {
log.error("Error opening TTL File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//PFT will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
const rawTtlData: DecodedTtlLine = deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"G_TTL_AMT",
"G_BETT_AMT",
"G_RPD_AMT",
"G_DED_AMT",
"G_CUST_AMT",
"G_AA_AMT",
"N_TTL_AMT",
"PREV_NET",
"SUPP_AMT",
"N_SUPP_AMT", //Previously commented. Possible issue.
"G_UPD_AMT",
"G_TTL_DISC",
"G_TAX",
"GST_AMT",
]),
);
//Apply business logic transfomrations.
return {
clm_total: rawTtlData.g_ttl_amt || 0,
depreciation_taxes: rawTtlData.g_bett_amt || 0, //TODO: Find where this needs to be filled from
cieca_ttl: { data: rawTtlData },
};
};
export default DecodeTtl;

View File

@@ -0,0 +1,64 @@
import { UUID } from "crypto";
export interface DecodedVeh {
// Basic vehicle information
plate_no?: string;
plate_st?: string;
v_vin?: string;
v_model_yr?: string;
v_make_desc?: string;
v_model_desc?: string;
v_color?: string;
kmin?: number;
area_of_damage?: {
impact1?: string;
impact2?: string;
};
// Complete vehicle data object
vehicle?: { data: VehicleRecordInterface };
}
export interface VehicleRecordInterface {
// Area of damage information
area_of_damage?: {
impact1?: string;
impact2?: string;
};
// Paint code information
v_paint_codes: {
paint_cd1: string;
paint_cd2: string;
paint_cd3: string;
};
// Vehicle information from DBF file
db_v_code?: string;
plate_no?: string;
plate_st?: string;
v_vin?: string;
v_cond: string;
v_prod_dt?: Date;
v_model_yr: string;
v_makecode: string;
v_make_desc?: string;
v_model?: string;
v_model_desc?: string;
v_type: string;
v_bstyle?: string;
v_trimcode?: string;
trim_color?: string;
v_mldgcode?: string;
v_engine?: string;
v_mileage?: number; //TODO: This can sometimes come in as UNK.
v_color?: string;
v_tone?: string;
v_stage?: string;
shopid: UUID;
//These are removed during business logic processing.
v_makedesc?: string;
impact_1?: string;
impact_2?: string;
paint_cd1?: string;
paint_cd2?: string;
paint_cd3?: string;
}

View File

@@ -0,0 +1,145 @@
import { DBFFile } from "dbffile";
import log from "electron-log/main";
import _ from "lodash";
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
import { DecodedVeh, VehicleRecordInterface } from "./decode-veh.interface";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import typeCaster from "../../util/typeCaster";
import { platform } from "@electron-toolkit/utils";
import { findFileCaseInsensitive } from "./decoder-utils";
const DecodeVeh = async (
extensionlessFilePath: string,
): Promise<DecodedVeh> => {
let dbf: DBFFile | null = null;
if (platform.isWindows) {
try {
dbf = await DBFFile.open(`${extensionlessFilePath}V.VEH`);
} catch (error) {
log.error("Error opening VEH File.", errorTypeCheck(error));
dbf = await DBFFile.open(`${extensionlessFilePath}.VEH`);
log.log("Found VEH file using regular CIECA Id.");
}
if (!dbf) {
log.error(`Could not find any VEH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any VEH files at ${extensionlessFilePath}`,
);
}
} else {
const possibleExtensions: string[] = ["v.veh", ".veh"];
const filePath = await findFileCaseInsensitive(
extensionlessFilePath,
possibleExtensions,
);
try {
if (!filePath) {
log.error(`Could not find any VEH files at ${extensionlessFilePath}`);
throw new Error(
`Could not find any VEH files at ${extensionlessFilePath}`,
);
}
dbf = await DBFFile.open(filePath, { readMode: "loose" });
} catch (error) {
log.error("Error opening VEH File.", errorTypeCheck(error));
throw error;
}
}
const rawDBFRecord = await dbf.readRecords(1);
//AD2 will always have only 1 row.
//Commented lines have been cross referenced with existing partner fields.
//typeCaster is required as the previous partner sent some of these values toString, and the database was made accordingly rather than keeping their original type.
//Alternative is to change the database schema to match the original type.
const rawVehData: VehicleRecordInterface = typeCaster(
deepLowerCaseKeys(
_.pick(rawDBFRecord[0], [
//TODO: Add typings for EMS File Formats.
"IMPACT_1",
"IMPACT_2",
"DB_V_CODE",
"PLATE_NO",
"PLATE_ST",
"V_VIN",
"V_COND",
"V_PROD_DT",
"V_MODEL_YR",
"V_MAKECODE",
"V_MAKEDESC",
"V_MODEL",
"V_TYPE",
"V_BSTYLE",
"V_TRIMCODE",
"TRIM_COLOR",
"V_MLDGCODE",
"V_ENGINE",
"V_MILEAGE",
"V_COLOR",
"V_TONE",
"V_STAGE",
"PAINT_CD1",
"PAINT_CD2",
"PAINT_CD3",
]),
),
{
v_tone: "string",
v_stage: "string",
},
);
//Apply business logic transfomrations.
//An old error where the column had an extra underscore.
rawVehData.v_make_desc = rawVehData.v_makedesc || rawVehData.v_makecode; //Fallback for US.
delete rawVehData.v_makedesc;
//An old error where the column had an extra underscore.
rawVehData.v_model_desc = rawVehData.v_model;
delete rawVehData.v_model;
//Consolidate Area of Damage.
const area_of_damage = {
impact1: rawVehData.impact_1 ?? "",
impact2: rawVehData.impact_2 ?? "",
};
delete rawVehData.impact_1;
delete rawVehData.impact_2;
const kmin = rawVehData.v_mileage ?? 0;
delete rawVehData.v_mileage;
//Consolidate Paint Code information.
rawVehData.v_paint_codes = {
paint_cd1: rawVehData.paint_cd1 ?? "",
paint_cd2: rawVehData.paint_cd2 ?? "",
paint_cd3: rawVehData.paint_cd3 ?? "",
};
delete rawVehData.paint_cd1;
delete rawVehData.paint_cd2;
delete rawVehData.paint_cd3;
rawVehData.shopid = store.get("app.bodyshop.id");
//Aggregate the vehicle data to be stamped onto the job record.
const jobVehicleData: DecodedVeh = {
plate_no: rawVehData.plate_no,
plate_st: rawVehData.plate_st,
v_vin: rawVehData.v_vin,
v_model_yr: rawVehData.v_model_yr,
v_make_desc: rawVehData.v_make_desc,
v_model_desc: rawVehData.v_model_desc,
v_color: rawVehData.v_color,
kmin: kmin,
area_of_damage: area_of_damage,
vehicle: {
data: rawVehData,
},
};
return jobVehicleData;
};
export default DecodeVeh;

View File

@@ -0,0 +1,47 @@
import log from "electron-log/main";
import fs from "fs";
import path from "path";
const findFileCaseInsensitive = async (
extensionlessFilePath: string,
extensions: string[],
): Promise<string | null> => {
const directory: string = path.dirname(extensionlessFilePath);
try {
const matchingFiles = fs.readdirSync(directory).filter((file: string) => {
return (
extensions.some((ext) =>
file.toLowerCase().endsWith(ext.toLowerCase()),
) &&
path
.basename(file, path.extname(file))
.toLowerCase()
.startsWith(path.basename(extensionlessFilePath).toLowerCase())
);
});
const files: string[] = [];
matchingFiles.forEach((file) => {
const fullPath = path.join(directory, file);
files.push(fullPath);
});
// Return the first matching file if needed
if (files.length > 0) {
return files[0];
}
} catch (error) {
log.error(`Failed to read directory ${directory}:`, error);
throw error;
}
return null;
};
const getFilePathWithoutExtension = (filePath: string): string => {
return path.join(
path.dirname(filePath),
path.basename(filePath, path.extname(filePath)),
);
};
export { findFileCaseInsensitive };

431
src/main/decoder/decoder.ts Normal file
View File

@@ -0,0 +1,431 @@
import { platform } from "@electron-toolkit/utils";
import { UUID } from "crypto";
import { Notification, shell } from "electron";
import log from "electron-log/main";
import fs from "fs";
import _ from "lodash";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
import client from "../graphql/graphql-client";
import {
INSERT_AVAILABLE_JOB_TYPED,
InsertAvailableJobResult,
QUERY_JOB_BY_CLM_NO_TYPED,
QUERY_VEHICLE_BY_VIN_TYPED,
QueryJobByClmNoResult,
VehicleQueryResult,
} from "../graphql/queries";
import store from "../store/store";
import DecodeAD1 from "./decode-ad1";
import { DecodedAd1 } from "./decode-ad1.interface";
import DecodeAD2 from "./decode-ad2";
import { DecodedAD2 } from "./decode-ad2.interface";
import DecodeEnv from "./decode-env";
import { DecodedEnv } from "./decode-env.interface";
import DecodeLin from "./decode-lin";
import { DecodedLin } from "./decode-lin.interface";
import DecodePfh from "./decode-pfh";
import { DecodedPfh } from "./decode-pfh.interface";
import DecodePfl from "./decode-pfl";
import { DecodedPfl } from "./decode-pfl.interface";
import DecodePfm from "./decode-pfm";
import { DecodedPfm } from "./decode-pfm.interface";
import DecodePfo from "./decode-pfo";
import { DecodedPfo } from "./decode-pfo.interface";
import DecodePfp from "./decode-pfp";
import { DecodedPfp } from "./decode-pfp.interface";
import DecodePft from "./decode-pft";
import { DecodedPft } from "./decode-pft.interface";
import DecodeStl from "./decode-stl";
import { DecodedStl } from "./decode-stl.interface";
import DecodeTtl from "./decode-ttl";
import { DecodedTtl } from "./decode-ttl.interface";
import DecodeVeh from "./decode-veh";
import { DecodedVeh } from "./decode-veh.interface";
import setAppProgressbar from "../util/setAppProgressBar";
import UploadEmsToS3 from "./emsbackup";
async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name,
);
log.debug("Importing Job", extensionlessFilePath);
try {
await WaitForAllFiles(extensionlessFilePath, requiredExtensions);
//The below all end up returning parts of the job object.
//Some of them return additional info - e.g. owner or vehicle record data at both the job and corresponding table level.
setAppProgressbar(0.1);
const env: DecodedEnv = await DecodeEnv(extensionlessFilePath);
setAppProgressbar(0.15);
const ad1: DecodedAd1 = await DecodeAD1(extensionlessFilePath);
setAppProgressbar(0.2);
const ad2: DecodedAD2 = await DecodeAD2(extensionlessFilePath);
setAppProgressbar(0.25);
const veh: DecodedVeh = await DecodeVeh(extensionlessFilePath);
setAppProgressbar(0.3);
const lin: DecodedLin = await DecodeLin(extensionlessFilePath);
setAppProgressbar(0.35);
const pfh: DecodedPfh = await DecodePfh(extensionlessFilePath);
setAppProgressbar(0.4);
const pfl: DecodedPfl = await DecodePfl(extensionlessFilePath);
setAppProgressbar(0.45);
const pft: DecodedPft = await DecodePft(extensionlessFilePath);
setAppProgressbar(0.5);
const pfm: DecodedPfm = await DecodePfm(extensionlessFilePath);
setAppProgressbar(0.55);
const pfo: DecodedPfo = await DecodePfo(extensionlessFilePath); // TODO: This will be the `cieca_pfo` object
setAppProgressbar(0.6);
const stl: DecodedStl = await DecodeStl(extensionlessFilePath); // TODO: This will be the `cieca_stl` object
setAppProgressbar(0.65);
const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath);
setAppProgressbar(0.7);
const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath);
setAppProgressbar(0.75);
const jobObjectUncleaned: RawJobDataObject = {
...env,
...ad1,
...ad2,
...veh,
...lin,
...pfh,
...pfl,
...pft,
...pfm,
...pfo,
...stl,
...ttl,
...pfp,
shopid: store.get("app.bodyshop.id") as UUID,
};
// Replace owner information with claimant information if necessary
const jobObject = ReplaceOwnerInfoWithClaimant(jobObjectUncleaned);
setAppProgressbar(0.8);
if (import.meta.env.DEV) {
// Save jobObject to a timestamped JSON file
const timestamp = new Date()
.toISOString()
.replace(/:/g, "-")
.replace(/\..+/, "");
const fileName = `job_${timestamp}_${parsedFilePath.name}.json`;
const logsDir = path.join(process.cwd(), "logs");
// Create logs directory if it doesn't exist
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const filePath = path.join(logsDir, fileName);
fs.writeFileSync(filePath, JSON.stringify(jobObject, null, 2), "utf8");
log.info(`Job data saved to: ${filePath}`);
}
const newAvailableJob: AvailableJobSchema = {
uploaded_by: store.get("user.email"),
bodyshopid: store.get("app.bodyshop.id"),
cieca_id: jobObject.ciecaid,
est_data: jobObject,
ownr_name: `${jobObject.ownr_fn} ${jobObject.ownr_ln} ${jobObject.ownr_co_nm}`,
ins_co_nm: jobObject.ins_co_nm,
vehicle_info: `${jobObject.v_model_yr} ${jobObject.v_make_desc} ${jobObject.v_model_desc}`,
clm_no: jobObject.clm_no,
clm_amt: jobObject.clm_total,
// source_system: jobObject.source_system, //TODO: Add back source system if needed.
issupplement: false,
jobid: null,
};
setAppProgressbar(0.85);
const existingVehicleRecord: VehicleQueryResult = await client.request(
QUERY_VEHICLE_BY_VIN_TYPED,
{
vin: jobObject.v_vin,
},
);
if (existingVehicleRecord.vehicles.length > 0) {
delete newAvailableJob.est_data.vehicle;
newAvailableJob.est_data.vehicleid = existingVehicleRecord.vehicles[0].id;
}
console.log("Available Job record to upload;", newAvailableJob);
setAppProgressbar(0.95);
if (jobObject.clm_no) {
const existingJobRecord: QueryJobByClmNoResult = await client.request(
QUERY_JOB_BY_CLM_NO_TYPED,
{ clm_no: jobObject.clm_no },
);
if (existingJobRecord.jobs.length > 0) {
newAvailableJob.issupplement = true;
newAvailableJob.jobid = existingJobRecord.jobs[0].id;
}
}
const insertRecordResult: InsertAvailableJobResult = await client.request(
INSERT_AVAILABLE_JOB_TYPED,
{
jobInput: [newAvailableJob],
},
);
setAppProgressbar(-1);
const uploadNotification = new Notification({
title: "Job Imported",
//subtitle: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}`,
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}. Click to view.`,
actions: [{ text: "View Job", type: "button" }],
});
uploadNotification.on("click", () => {
shell.openExternal(
`${
store.get("app.isTest")
? import.meta.env.VITE_FE_URL_TEST
: import.meta.env.VITE_FE_URL
}/manage/available`,
);
});
uploadNotification.show();
log.debug("Job inserted", insertRecordResult);
UploadEmsToS3({
extensionlessFilePath,
bodyshopid: newAvailableJob.bodyshopid,
ciecaid: jobObject.ciecaid ?? "",
clm_no: jobObject.clm_no ?? "",
ownr_ln: jobObject.ownr_ln ?? "",
});
} catch (error) {
log.error("Error encountered while decoding job. ", errorTypeCheck(error));
const uploadNotificationFailure = new Notification({
title: "Job Upload Failure",
body: errorTypeCheck(error).message, //TODO: Remove after debug.
});
uploadNotificationFailure.show();
}
}
export default ImportJob;
export interface RawJobDataObject
extends DecodedEnv,
DecodedAd1,
DecodedAD2,
DecodedVeh,
DecodedLin,
DecodedPfh,
DecodedPfl,
DecodedPft,
DecodedPfm,
DecodedPfo,
DecodedStl,
DecodedTtl,
DecodedPfp {
vehicleid?: UUID;
shopid: UUID;
}
export interface AvailableJobSchema {
uploaded_by: string;
bodyshopid: UUID;
cieca_id?: string;
est_data: RawJobDataObject;
ownr_name: string;
ins_co_nm?: string;
vehicle_info: string;
clm_no?: string;
clm_amt: number;
source_system?: string | null;
issupplement: boolean;
jobid: UUID | null;
}
async function WaitForAllFiles(
baseFilePath: string,
requiredExtensions: string[],
maxRetries: number = 5,
backoffMs: number = 1000,
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
//Get all files in directory if Mac.
let filesInDir: string[] = [];
if (platform.isMacOS) {
const dir: string = path.dirname(baseFilePath);
filesInDir = fs.readdirSync(dir).map((file) => file.toLowerCase());
}
const missingFiles = requiredExtensions.filter((ext) => {
const filePath: string = `${baseFilePath}.${ext}`;
const filePathA: string = `${baseFilePath}A.${ext}`;
const filePathB: string = `${baseFilePath}B.${ext}`;
const filePathV: string = `${baseFilePath}V.${ext}`;
if (!platform.isWindows) {
// Case-insensitive check for macOS/Linux
const baseName: string = path.basename(baseFilePath);
return !(
filesInDir.includes(`${baseName}.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}A.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}B.${ext}`.toLowerCase()) ||
filesInDir.includes(`${baseName}V.${ext}`.toLowerCase())
);
} else {
// Case-sensitive check for other platforms
return !(
fs.existsSync(filePath) ||
fs.existsSync(filePathA) ||
fs.existsSync(filePathB) ||
fs.existsSync(filePathV)
);
}
});
if (missingFiles.length === 0) {
return; // All files are present
}
log.debug(
`Attempt ${attempt}: Missing files: ${missingFiles.join(", ")}. Retrying in ${backoffMs}ms...`,
);
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, backoffMs));
backoffMs *= 2; // Exponential backoff
} else {
throw new Error(
`The set of files is not valid. Missing files for CIECA ID ${baseFilePath}: ${missingFiles.join(", ")}`,
);
}
}
}
const requiredExtensions = [
"env",
"ad1",
"ad2",
"veh",
"lin",
"pfh",
"pfl",
"pft",
"pfm",
"pfo",
"stl",
"ttl",
"pfp",
];
export function ReplaceOwnerInfoWithClaimant<
T extends Partial<
Pick<
RawJobDataObject,
| "ownr_ln"
| "ownr_fn"
| "ownr_co_nm"
| "ownr_title"
| "ownr_co_nm"
| "ownr_addr1"
| "ownr_addr2"
| "ownr_city"
| "ownr_st"
| "ownr_zip"
| "ownr_ctry"
| "ownr_ph1"
| "ownr_ph2"
| "ownr_ea"
| "clmt_ln"
| "clmt_fn"
| "clmt_title"
| "clmt_co_nm"
| "clmt_addr1"
| "clmt_addr2"
| "clmt_city"
| "clmt_st"
| "clmt_zip"
| "clmt_ctry"
| "clmt_ph1"
| "clmt_ph2"
| "clmt_ea"
| "insd_ln"
| "insd_fn"
| "insd_title"
| "insd_co_nm"
| "insd_addr1"
| "insd_addr2"
| "insd_city"
| "insd_st"
| "insd_zip"
| "insd_ctry"
| "insd_ph1"
| "insd_ph2"
| "insd_ea"
| "owner"
>
>,
>(jobObject: T): T {
// Promote claimant data first if owner identity is entirely missing; otherwise fallback to insured data.
const identityKeys = ["ln", "fn", "co_nm"] as const; // keys used to determine presence
const copyKeys = [
"ln",
"fn",
"title",
"co_nm",
"addr1",
"addr2",
"city",
"st",
"zip",
"ctry",
"ph1",
"ph2",
"ea",
] as const; // full set of fields to copy/delete
const ownerMissing = identityKeys.every((k) =>
_.isEmpty((jobObject as any)[`ownr_${k}`]),
);
const claimantHasSome = identityKeys.some(
(k) => !_.isEmpty((jobObject as any)[`clmt_${k}`]),
);
const claimantMissing = identityKeys.every((k) =>
_.isEmpty((jobObject as any)[`clmt_${k}`]),
);
const { owner } = jobObject as any; // destructure for optional nested updates
// Copy helper inline (no extra function as requested)
const promote = (sourcePrefix: "clmt" | "insd"): void => {
copyKeys.forEach((suffix) => {
(jobObject as any)[`ownr_${suffix}`] = (jobObject as any)[
`${sourcePrefix}_${suffix}`
];
if (owner?.data) {
owner.data[`ownr_${suffix}`] = (jobObject as any)[
`${sourcePrefix}_${suffix}`
];
}
});
};
if (ownerMissing && claimantHasSome) {
promote("clmt");
} else if (ownerMissing && claimantMissing) {
promote("insd");
}
// Delete the claimant info as it's not needed.
copyKeys.forEach((suffix) => delete (jobObject as any)[`clmt_${suffix}`]);
// Delete the insured info as it's not needed.
copyKeys.forEach((suffix) => delete (jobObject as any)[`insd_${suffix}`]);
return jobObject;
}

View File

@@ -0,0 +1,104 @@
import axios from "axios";
import archiver from "archiver";
import errorTypeCheck from "../../util/errorTypeCheck";
import { UUID } from "crypto";
import fs from "fs";
import path from "path";
import stream from "stream";
import { getTokenFromRenderer } from "../graphql/graphql-client";
import store from "../store/store";
async function UploadEmsToS3({
extensionlessFilePath,
bodyshopid,
clm_no,
ciecaid,
ownr_ln,
}: {
extensionlessFilePath: string;
bodyshopid: UUID;
clm_no: string;
ciecaid: string;
ownr_ln: string;
}): Promise<boolean> {
// This function is a placeholder for the actual upload logic
try {
const directory = path.dirname(extensionlessFilePath);
const baseFilename = path.basename(extensionlessFilePath);
// Find all files in the directory that start with the base filename
const filesToZip = fs
.readdirSync(directory)
.filter((file) => file.startsWith(baseFilename))
.map((file) => path.join(directory, file));
if (filesToZip.length === 0) {
console.error("No files found to zip.");
return false;
}
// Create a zip archive in memory
const archive = archiver("zip", { zlib: { level: 9 } });
const zipBuffer = await new Promise<Buffer>((resolve, reject) => {
const buffers: Buffer[] = [];
const writableStream = new stream.Writable({
write(chunk, _encoding, callback) {
buffers.push(chunk);
callback();
},
});
writableStream.on("finish", () => resolve(Buffer.concat(buffers)));
writableStream.on("error", reject);
archive.pipe(writableStream);
// Append files to the archive
filesToZip.forEach((file) => {
archive.file(file, { name: path.basename(file) });
});
archive.finalize();
});
// Get the presigned URL from the server
const presignedUrlResponse = await axios.post(
`${
store.get("app.isTest")
? import.meta.env.VITE_API_TEST_URL
: import.meta.env.VITE_API_URL
}/emsupload`,
{
bodyshopid,
ciecaid,
clm_no,
ownr_ln,
},
{
headers: {
Authorization: `Bearer ${await getTokenFromRenderer()}`,
},
},
);
const presignedUrl = presignedUrlResponse.data?.presignedUrl;
if (!presignedUrl) {
console.error("Failed to retrieve presigned URL.");
return false;
}
// Upload the zip file to S3 using the presigned URL
await axios.put(presignedUrl, zipBuffer, {
headers: {
"Content-Type": "application/zip",
},
});
} catch (error) {
console.error("Error uploading EMS to S3:", errorTypeCheck(error));
return false;
}
return true; // Return true if the upload is successful
}
export default UploadEmsToS3;

View File

@@ -0,0 +1,57 @@
import path from "path";
import { GetAllEnvFiles } from "../watcher/watcher";
import DecodeAD1 from "./decode-ad1";
import DecodeAD2 from "./decode-ad2";
import DecodeEnv from "./decode-env";
import DecodeVeh from "./decode-veh";
import { ReplaceOwnerInfoWithClaimant } from "./decoder";
const folderScan = async (): Promise<FolderScanResult[]> => {
//Get all ENV files for watched paths.
const allEnvFiles = GetAllEnvFiles();
//Run a simplified decode on them
const returnedFiles: FolderScanResult[] = [];
for (const filepath of allEnvFiles) {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name,
);
const rawJob = {
...(await DecodeEnv(extensionlessFilePath)),
...(await DecodeAD1(extensionlessFilePath)),
...(await DecodeAD2(extensionlessFilePath)),
...(await DecodeVeh(extensionlessFilePath)),
};
const job = ReplaceOwnerInfoWithClaimant(rawJob);
const scanResult: FolderScanResult = {
id: job.ciecaid,
filepath: filepath,
cieca_id: job.ciecaid,
clm_no: job.clm_no,
owner: `${job.ownr_fn} ${job.ownr_ln} ${job.ownr_co_nm}`.trim(),
vehicle:
`${job.vehicle?.data.v_model_yr} ${job.vehicle?.data.v_make_desc} ${job.vehicle?.data.v_model_desc}`.trim(),
ins_co_nm: job.ins_co_nm,
};
returnedFiles.push(scanResult);
}
//Build up the object and return it
return returnedFiles;
};
export interface FolderScanResult {
id?: string;
filepath: string;
cieca_id?: string;
clm_no?: string;
owner: string;
ins_co_nm?: string;
vehicle: string;
}
export default folderScan;

View File

@@ -0,0 +1,158 @@
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

@@ -0,0 +1,67 @@
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

@@ -0,0 +1,80 @@
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

@@ -0,0 +1,85 @@
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

@@ -0,0 +1,59 @@
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

@@ -0,0 +1,302 @@
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

@@ -0,0 +1,105 @@
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

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,39 @@
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

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,40 @@
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

@@ -0,0 +1,36 @@
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

@@ -0,0 +1,65 @@
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

@@ -0,0 +1,83 @@
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

@@ -0,0 +1,322 @@
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

@@ -0,0 +1,53 @@
import { BrowserWindow, ipcMain } from "electron";
import log from "electron-log/main";
import { GraphQLClient, RequestMiddleware } from "graphql-request";
import errorTypeCheck from "../../util/errorTypeCheck.js";
import ipcTypes from "../../util/ipcTypes.json";
import store from "../store/store.js";
import getMainWindow from "../../util/getMainWindow.js";
const requestMiddleware: RequestMiddleware = async (request) => {
const token = await getTokenFromRenderer();
log.info(
`%c[Graphql Request]%c${request.operationName}`,
"color: red",
"color: green",
request,
);
return {
...request,
headers: { ...request.headers, Authorization: `Bearer ${token}` },
};
};
const client: GraphQLClient = new GraphQLClient(
store.get("app.isTest") || false
? import.meta.env.VITE_GRAPHQL_ENDPOINT_TEST
: import.meta.env.VITE_GRAPHQL_ENDPOINT,
{
requestMiddleware,
},
);
export async function getTokenFromRenderer(): Promise<string> {
return new Promise((resolve) => {
try {
const mainWindow = getMainWindow();
//TODO: Verify that this will work if the app is minimized/closed.
mainWindow?.webContents.send(ipcTypes.toRenderer.user.getToken);
} catch (error) {
log.error(
"Unable to send request to renderer process for token",
errorTypeCheck(error),
);
}
// Set up one-time listener for the response
ipcMain.once(ipcTypes.toMain.user.getTokenResponse, (_, token: string) => {
resolve(token);
});
});
}
export default client;

272
src/main/graphql/queries.ts Normal file
View File

@@ -0,0 +1,272 @@
import { UUID } from "crypto";
import { parse, TypedQueryDocumentNode } from "graphql";
import { gql } from "graphql-request";
import { AvailableJobSchema } from "../decoder/decoder";
// Define types for the query result and variables
export interface ActiveBodyshopQueryResult {
bodyshops: Array<{
id: string;
shopname: string;
region_config: string;
convenient_company: string;
}>;
}
// No variables needed for this query
export const QUERY_ACTIVE_BODYSHOP_TYPED: TypedQueryDocumentNode<
ActiveBodyshopQueryResult,
Record<never, never>
> = parse(gql`
query QUERY_ACTIVE_BODYSHOP {
bodyshops(where: { associations: { active: { _eq: true } } }) {
id
shopname
region_config
convenient_company
}
}
`) as TypedQueryDocumentNode<ActiveBodyshopQueryResult, Record<never, never>>;
export interface MasterdataQueryResult {
masterdata: Array<{
value: string;
key: string;
}>;
}
interface MasterdataQueryVariables {
key: string;
}
export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode<
MasterdataQueryResult,
MasterdataQueryVariables
> = parse(gql`
query QUERY_MASTERDATA($key: String!) {
masterdata(where: { key: { _eq: $key } }) {
value
key
}
}
`) as TypedQueryDocumentNode<MasterdataQueryResult, MasterdataQueryVariables>;
export interface VehicleQueryResult {
vehicles: Array<{
id: UUID;
}>;
}
interface VehicleQueryVariables {
vin: string;
}
export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode<
VehicleQueryResult,
VehicleQueryVariables
> = parse(gql`
query QUERY_VEHICLE_BY_VIN($vin: String!) {
vehicles(where: { v_vin: { _eq: $vin } }) {
id
}
}
`) as TypedQueryDocumentNode<VehicleQueryResult, VehicleQueryVariables>;
export interface QueryJobByClmNoResult {
jobs: Array<{
id: UUID;
}>;
}
export interface QueryJobByClmNoVariables {
clm_no: string;
}
export const QUERY_JOB_BY_CLM_NO_TYPED: TypedQueryDocumentNode<
QueryJobByClmNoResult,
QueryJobByClmNoVariables
> = parse(gql`
query QUERY_JOB_BY_CLM_NO($clm_no: String!) {
jobs(where: { clm_no: { _eq: $clm_no } }) {
id
}
}
`) as TypedQueryDocumentNode<QueryJobByClmNoResult, QueryJobByClmNoVariables>;
export interface InsertAvailableJobResult {
returning: Array<{
id: UUID;
}>;
}
export interface InsertAvailableJobVariables {
jobInput: Array<AvailableJobSchema>;
}
export const INSERT_AVAILABLE_JOB_TYPED: TypedQueryDocumentNode<
InsertAvailableJobResult,
InsertAvailableJobVariables
> = parse(gql`
mutation INSERT_AVAILABLE_JOB($jobInput: [available_jobs_insert_input!]!) {
insert_available_jobs(
objects: $jobInput
on_conflict: {
constraint: available_jobs_clm_no_bodyshopid_key
update_columns: [
clm_amt
cieca_id
est_data
issupplement
ownr_name
source_system
supplement_number
vehicle_info
]
}
) {
returning {
id
}
}
}
`) as TypedQueryDocumentNode<
InsertAvailableJobResult,
InsertAvailableJobVariables
>;
// Add PpgData Query
export interface PpgDataQueryResult {
bodyshops_by_pk: {
id: string;
shopname: string;
imexshopid: string;
} | null;
jobs: Array<{
id: string;
ro_number: string;
status: string;
ownr_fn: string;
ownr_ln: string;
ownr_co_nm: string;
v_vin: string;
v_model_yr: string;
v_make_desc: string;
v_model_desc: string;
v_color: string;
plate_no: string;
ins_co_nm: string;
est_ct_fn: string;
est_ct_ln: string;
rate_mapa: number;
rate_lab: number;
job_totals: {
rates?: {
mapa?: {
total?: {
amount?: number;
};
};
};
totals?: {
subtotal?: {
amount?: number;
};
};
};
vehicle: {
v_paint_codes: {
paint_cd1?: string;
};
};
labhrs: {
aggregate: {
sum: {
mod_lb_hrs: number;
};
};
};
larhrs: {
aggregate: {
sum: {
mod_lb_hrs: number;
};
};
};
}>;
}
export interface PpgDataQueryVariables {
today: string;
todayplus5: string;
shopid: string;
}
export const PPG_DATA_QUERY_TYPED: TypedQueryDocumentNode<
PpgDataQueryResult,
PpgDataQueryVariables
> = parse(gql`
query PpgData(
$today: timestamptz!
$todayplus5: timestamptz!
$shopid: uuid!
) {
bodyshops_by_pk(id: $shopid) {
id
shopname
imexshopid
}
jobs(
where: {
_or: [
{
_and: [
{ scheduled_in: { _lte: $todayplus5 } }
{ scheduled_in: { _gte: $today } }
]
}
{ inproduction: { _eq: true } }
]
}
) {
id
ro_number
status
ownr_fn
ownr_ln
ownr_co_nm
v_vin
v_model_yr
v_make_desc
v_model_desc
v_color
plate_no
ins_co_nm
est_ct_fn
est_ct_ln
rate_mapa
rate_lab
job_totals
vehicle {
v_paint_codes
}
labhrs: joblines_aggregate(
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}
`) as TypedQueryDocumentNode<PpgDataQueryResult, PpgDataQueryVariables>;

View File

@@ -0,0 +1,210 @@
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");
}
}
}

19
src/main/index.test.ts Normal file
View File

@@ -0,0 +1,19 @@
import { _electron as electron } from "playwright";
import { test, expect } from "@playwright/test";
test("Basic Electron app compilation.", async () => {
const electronApp = await electron.launch({ args: ["."] });
const isPackaged = await electronApp.evaluate(async ({ app }) => {
// This runs in Electron's main process, parameter here is always
// the result of the require('electron') in the main app script.
return app.isPackaged;
});
expect(isPackaged).toBe(false);
// Wait for the first BrowserWindow to open
// and return its Page object
const window = await electronApp.firstWindow();
// close app
await electronApp.close();
});

691
src/main/index.ts Normal file
View File

@@ -0,0 +1,691 @@
import { is, optimizer, platform } from "@electron-toolkit/utils";
import Sentry from "@sentry/electron/main";
import {
app,
BrowserWindow,
globalShortcut,
ipcMain,
Menu,
nativeImage,
shell,
Tray,
} from "electron";
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,
default as errorTypeCheck,
} 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 {
isKeepAliveAgentInstalled,
setupKeepAliveAgent,
} from "./setup-keep-alive-agent";
import {
isKeepAliveTaskInstalled,
setupKeepAliveTask,
} from "./setup-keep-alive-task";
import ensureWindowOnScreen from "./util/ensureWindowOnScreen";
import ongoingMemoryDump, { dumpMemoryStatsToFile } from "../util/memUsage";
const appIconToUse =
import.meta.env.VITE_COMPANY === "IMEX" ? imexAppIcon : romeAppIcon;
Sentry.init({
dsn: "https://ba41d22656999a8c1fd63bcb7df98650@o492140.ingest.us.sentry.io/4509074139447296",
});
log.initialize();
// Configure log format to include process ID
log.transports.file.format =
"[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] [PID:{processId}] {text}";
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";
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) {
log.warn(
"Another instance is already running and could not obtain mutex lock. Exiting this instance.",
);
isAppQuitting = true;
app.quit(); // Quit the app if another instance is already running
}
function createWindow(): void {
// Create the browser window.
const { width, height, x, y } = store.get("app.windowBounds") as {
width: number;
height: number;
x: number | undefined;
y: number | undefined;
};
// Validate window position is on screen
const { validX, validY } = ensureWindowOnScreen(x, y, width, height);
const mainWindow = new BrowserWindow({
width,
height,
x: validX,
y: validY,
show: false, // Start hidden, show later if not keep-alive
minWidth: 600,
minHeight: 400,
//autoHideMenuBar: true,
...(process.platform === "linux"
? {
icon: appIconToUse,
}
: {}),
title: "Shop Partner",
webPreferences: {
preload: join(__dirname, "../preload/index.js"),
sandbox: false,
devTools: true,
},
});
const template: Electron.MenuItemConstructorOptions[] = [
// { role: 'appMenu' }
// @ts-ignore
...(isMac
? [
{
label: app.name,
submenu: [
{ role: "about" },
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
],
},
]
: []),
// { role: 'fileMenu' }
{
label: "File",
submenu: [
// @ts-ignore
...(!isMac ? [{ role: "about" }] : []),
// @ts-ignore
isMac ? { role: "close" } : { role: "quit" },
],
},
// { role: 'editMenu' }
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
// @ts-ignore
...(isMac
? [
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{
label: "Speech",
submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }],
},
]
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
],
},
// { role: 'viewMenu' }
{
label: "View",
// @ts-ignore
submenu: [
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
{
label: "Application",
// @ts-ignore
submenu: [
{
label: "Open on Startup",
checked: store.get("app.openOnStartup") as boolean,
type: "checkbox",
click: (): void => {
const currentSetting = store.get("app.openOnStartup") as boolean;
store.set("app.openOnStartup", !currentSetting);
log.info("Open on startup set to", !currentSetting);
if (!import.meta.env.DEV) {
app.setLoginItemSettings({
enabled: true, //This is a windows only command. Updates the task manager and registry.
openAtLogin: !currentSetting,
});
}
},
},
{
label: `Check for Updates (${app.getVersion()})`,
click: (): void => {
checkForAppUpdates();
},
},
{
label: "Development",
id: "development",
visible: import.meta.env.DEV,
submenu: [
{
label: "Connect to Test",
checked: store.get("app.isTest") as boolean,
type: "checkbox",
id: "toggleTest",
click: (): void => {
const currentSetting = store.get("app.isTest") as boolean;
store.set("app.isTest", !currentSetting);
log.info("Setting isTest to: ", !currentSetting);
app.relaunch(); // Relaunch the app
preQuitMethods(); //Quitting handlers aren't called. Manually execute to clean up the app.
app.exit(0); // Exit the current instance
},
},
{
label: "Check for updates",
click: (): void => {
checkForAppUpdates();
},
},
{
label: "Open Log File",
click: (): void => {
/* action for item 1 */
shell
.openPath(log.transports.file.getFile().path)
.catch((error) => {
log.error(
"Failed to open log file:",
errorTypeCheck(error),
);
});
},
},
{
label: "Clear Log",
click: (): void => {
log.transports.file.getFile().clear();
},
},
{
label: "Open Config Folder",
click: (): void => {
shell.openPath(path.dirname(store.path)).catch((error) => {
log.error(
"Failed to open config folder:",
errorTypeCheck(error),
);
});
},
},
{
label: "Log the Store",
click: (): void => {
log.debug(
"Store Contents" + JSON.stringify(store.store, null, 4),
);
},
},
{
type: "separator",
},
{
label: "Enable Memory Logging",
checked: store.get("settings.enableMemDebug") as boolean,
type: "checkbox",
click: (): void => {
const currentSetting = store.get(
"settings.enableMemDebug",
) as boolean;
store.set("settings.enableMemDebug", !currentSetting);
log.info("Enable Memory Logging set to", !currentSetting);
},
},
{
label: "Dump Memory Stats Now",
click: (): void => {
dumpMemoryStatsToFile();
},
},
{
type: "separator",
},
// {
// label: "Decode Hardcoded Estimate",
// click: (): void => {
// ImportJob(`C:\\EMS\\CCC\\9ee762f4.ENV`);
// },
// },
{
label: "Install Keep Alive",
enabled: true, // Default to enabled, update dynamically
click: async (): Promise<void> => {
try {
if (platform.isWindows) {
log.debug("Creating Windows keep-alive task");
await setupKeepAliveTask();
log.info("Successfully installed Windows keep-alive task");
} else if (platform.isMacOS) {
log.debug("Creating macOS keep-alive agent");
await setupKeepAliveAgent();
log.info("Successfully installed macOS keep-alive agent");
}
// Wait to ensure task/agent is registered
await new Promise((resolve) => setTimeout(resolve, 1500));
// Rebuild menu and update enabled state
await updateKeepAliveMenuItem();
} catch (error) {
log.error(
`Failed to install keep-alive: ${error instanceof Error ? error.message : String(error)}`,
);
// Optionally notify user (e.g., via dialog or log)
}
},
},
{
label: "Add All Estimates in watched directories",
click: (): void => {
GetAllEnvFiles().forEach((file) => ImportJob(file));
},
},
],
},
],
},
// { role: 'windowMenu' }
{
label: "Window",
submenu: [
{ role: "minimize" },
{ role: "zoom" },
// @ts-ignore
...(isMac
? [
{ type: "separator" },
{ role: "front" },
{ type: "separator" },
{ role: "window" },
]
: [{ role: "close" }]),
],
},
];
// Dynamically update Install Keep Alive enabled state
const updateKeepAliveMenuItem = async (): Promise<void> => {
try {
const isInstalled = platform.isWindows
? await isKeepAliveTaskInstalled()
: platform.isMacOS
? await isKeepAliveAgentInstalled()
: false;
const developmentMenu = template
.find((item) => item.label === "Application")
// @ts-ignore
?.submenu?.find((item: { id: string }) => item.id === "development")
?.submenu as Electron.MenuItemConstructorOptions[];
const keepAliveItem = developmentMenu?.find(
(item) => item.label === "Install Keep Alive",
);
if (keepAliveItem) {
keepAliveItem.enabled = !isInstalled; // Enable if not installed, disable if installed
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
log.debug(
`Updated Install Keep Alive menu item: enabled=${keepAliveItem.enabled}`,
);
}
} catch (error) {
log.error(
`Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`,
);
}
};
const menu: Electron.Menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
// Update menu item enabled state on app start
updateKeepAliveMenuItem().catch((error) => {
log.error(
`Error updating Keep Alive menu item: ${error instanceof Error ? error.message : String(error)}`,
);
});
// Register a global shortcut to show the hidden item
globalShortcut.register("CommandOrControl+Shift+T", () => {
console.log("Shortcut pressed! Revealing hidden item.");
// Update the menu to make the hidden item visible
// Find the menu item dynamically by its id
const fileMenu = template.find((item) => item.label === "Application");
// @ts-ignore
const hiddenItem = fileMenu?.submenu?.find(
(item: { id: string }) => item.id === "development",
);
//Adjust the development menu as well.
if (hiddenItem) {
hiddenItem.visible = true; // Update the visibility dynamically
const menu: Electron.Menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
});
// Store window properties for later
const storeWindowState = (): void => {
const [width, height] = mainWindow.getSize();
const [x, y] = mainWindow.getPosition();
store.set("app.windowBounds", { width, height, x, y });
};
mainWindow.on("resized", storeWindowState);
mainWindow.on("maximize", storeWindowState);
mainWindow.on("unmaximize", storeWindowState);
mainWindow.on("moved", storeWindowState);
mainWindow.on("ready-to-show", () => {
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) => {
if (!isAppQuitting) {
event.preventDefault(); // Prevent the window from closing
mainWindow.hide();
}
});
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url).catch((error) => {
log.error("Failed to open external URL:", errorTypeCheck(error));
});
return { action: "deny" };
});
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]).catch((error) => {
log.error("Failed to load URL:", errorTypeCheck(error));
});
} else {
mainWindow
.loadFile(join(__dirname, "../renderer/index.html"))
.catch((error) => {
log.error("Failed to load file:", errorTypeCheck(error));
});
}
if (import.meta.env.DEV) {
mainWindow.webContents.openDevTools();
}
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
log.debug("App is ready, initializing shortcuts and protocol handlers.");
if (platform.isWindows) {
app.setAppUserModelId("Shop Partner");
}
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
let isDefaultProtocolClient: boolean;
// remove so we can register each time as we run the app.
app.removeAsDefaultProtocolClient(protocol);
// If we are running a non-packaged version of the app && on windows
if (process.env.NODE_ENV === "development" && process.platform === "win32") {
// Set the path of electron.exe and your app.
// These two additional parameters are only available on windows.
isDefaultProtocolClient = app.setAsDefaultProtocolClient(
protocol,
process.execPath,
[path.resolve(process.argv[1])],
);
} else {
isDefaultProtocolClient = app.setAsDefaultProtocolClient(protocol);
}
if (isDefaultProtocolClient) {
log.info("Protocol handler registered successfully.");
} else {
log.warn("Failed to register protocol handler.");
}
//Dynamically load ipcMain handlers once ready.
try {
const { initializeCronTasks } = 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),
});
throw error; // Adjust based on whether the app should continue
}
//Create Tray
const trayicon = nativeImage.createFromPath(appIconToUse);
const tray = new Tray(trayicon.resize({ width: 16 }));
const contextMenu = Menu.buildFromTemplate([
{
label: "Show App",
click: (): void => {
openMainWindow();
},
},
{
label: "Quit",
click: (): void => {
app.quit(); // actually quit the app.
},
},
]);
tray.on("double-click", () => {
openMainWindow();
});
tray.setContextMenu(contextMenu);
//Check for app updates.
autoUpdater.logger = log;
autoUpdater.allowDowngrade = true;
// if (import.meta.env.DEV) {
// // Useful for some dev/debugging tasks, but download can
// // not be validated because dev app is not signed
// autoUpdater.channel = "alpha";
// autoUpdater.updateConfigPath = path.join(
// __dirname,
// "../../dev-app-update.yml",
// );
// autoUpdater.forceDevUpdateConfig = true;
// //autoUpdater.autoDownload = false;
// }
autoUpdater.on("checking-for-update", () => {
log.info("Checking for update...");
const mainWindow = getMainWindow();
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.checking);
});
autoUpdater.on("update-available", (info) => {
log.info("Update available.", info);
const mainWindow = getMainWindow();
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.available, info);
});
autoUpdater.on("download-progress", (progress) => {
log.info(`Download speed: ${progress.bytesPerSecond}`);
log.info(`Downloaded ${progress.percent}%`);
log.info(`Total downloaded ${progress.transferred}/${progress.total}`);
const mainWindow = getMainWindow();
mainWindow?.webContents.send(
ipcTypes.toRenderer.updates.downloading,
progress,
);
});
autoUpdater.on("update-downloaded", (info) => {
log.info("Update downloaded", info);
const mainWindow = getMainWindow();
mainWindow?.webContents.send(ipcTypes.toRenderer.updates.downloaded, info);
});
// Check if launched with keep-alive protocol (Windows)
const args = process.argv.slice(1);
if (args.some((arg) => arg.startsWith(`${protocol}://keep-alive`))) {
isKeepAliveLaunch = true;
}
//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();
});
});
app.on("open-url", (event: Electron.Event, url: string) => {
event.preventDefault();
if (url.startsWith(`${protocol}://keep-alive`)) {
log.info("Keep-alive protocol received.");
// Do nothing, whether app is running or not
return;
} else {
openInExplorer(url);
}
});
// Add this event handler for second instance
app.on("second-instance", (_event: Electron.Event, argv: string[]) => {
const url = argv.find((arg) => arg.startsWith(`${protocol}://`));
if (url) {
if (url.startsWith(`${protocol}://keep-alive`)) {
log.info(
"Keep-alive protocol received, app is already running. Nothing to do.",
);
// Do nothing if already running
return;
} else {
log.info("Received Media URL: ", url);
openInExplorer(url);
}
}
// No action taken if no URL is provided
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit(); //Disable the quit.
}
});
app.on("before-quit", () => {
preQuitMethods();
});
//We need to hit the prequit methods from here as well to ensure the app quits and restarts.
ipcMain.on(ipcTypes.toMain.updates.apply, () => {
log.info("Applying update from renderer.");
preQuitMethods();
setImmediate(() => {
app.removeAllListeners("window-all-closed");
const mainWindow = getMainWindow();
if (mainWindow) mainWindow.close();
autoUpdater.quitAndInstall(false);
});
});
function preQuitMethods(): void {
localServer.stop();
const currentSetting = store.get("app.openOnStartup") as boolean;
if (!import.meta.env.DEV) {
app.setLoginItemSettings({
enabled: true, //This is a windows only command. Updates the task manager and registry.
openAtLogin: !currentSetting,
});
}
globalShortcut.unregisterAll();
isAppQuitting = true;
}
function openMainWindow(): void {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.show();
} else {
createWindow();
}
}
function openInExplorer(url: string): void {
const folderPath: string = decodeURIComponent(url.split(`${protocol}://`)[1]);
log.info("Opening folder in explorer", folderPath);
shell.openPath(folderPath).catch((error) => {
log.error("Failed to open folder in explorer:", errorTypeCheck(error));
});
}

View File

@@ -0,0 +1,278 @@
import { app, ipcMain } from "electron";
import log from "electron-log/main";
import { autoUpdater } from "electron-updater";
import path from "path";
import ipcTypes from "../../util/ipcTypes.json";
import ImportJob from "../decoder/decoder";
import store from "../store/store";
import { StartWatcher, StopWatcher } from "../watcher/watcher";
import {
SettingEmsOutFilePathGet,
SettingEmsOutFilePathSet,
SettingsPaintScaleInputConfigsGet,
SettingsPaintScaleInputConfigsSet,
SettingsPaintScaleInputPathSet,
SettingsPaintScaleOutputConfigsGet,
SettingsPaintScaleOutputConfigsSet,
SettingsPaintScaleOutputPathSet,
SettingsPpcFilePathGet,
SettingsPpcFilePathSet,
SettingsWatchedFilePathsAdd,
SettingsWatchedFilePathsGet,
SettingsWatchedFilePathsRemove,
SettingsWatcherPollingGet,
SettingsWatcherPollingSet,
} from "./ipcMainHandler.settings";
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 => {
Object.keys(ipcTypes.toMain).forEach((key) => {
const messageType = ipcTypes.toMain[key];
const originalHandler = ipcMain.listeners(messageType)?.[0];
if (originalHandler) {
ipcMain.removeAllListeners(messageType);
}
ipcMain.on(messageType, (event, payload) => {
log.info(
`%c[IPC Main]%c${messageType}`,
"color: red",
"color: green",
payload,
);
if (originalHandler) {
originalHandler(event, payload);
}
});
});
};
// 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, () =>
console.log("** Verify that ipcMain is loaded and working."),
);
// Auth handler
ipcMain.on(ipcTypes.toMain.authStateChanged, ipcMainHandleAuthStateChanged);
ipcMain.on(ipcTypes.toMain.user.resetPassword, ipMainHandleResetPassword);
// Add debug handlers if in development
if (import.meta.env.DEV) {
log.debug("[IPC Debug Functions] Adding Debug Handlers");
ipcMain.on(ipcTypes.toMain.debug.decodeEstimate, async (): Promise<void> => {
const relativeEmsFilepath = `_reference/ems/MPI_1/3698420.ENV`;
const rootDir = app.getAppPath();
const absoluteFilepath = path.join(rootDir, relativeEmsFilepath);
log.debug("[IPC Debug Function] Decode test Estimate", absoluteFilepath);
await ImportJob(absoluteFilepath);
const job2 = `/Users/pfic/Downloads/12285264/2285264.ENV`;
const job3 = `/Users/pfic/Downloads/14033376/4033376.ENV`;
await ImportJob(job2);
await ImportJob(job3);
});
}
// Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.get,
SettingsWatchedFilePathsGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.add,
SettingsWatchedFilePathsAdd,
);
ipcMain.handle(
ipcTypes.toMain.settings.filepaths.remove,
SettingsWatchedFilePathsRemove,
);
ipcMain.handle(
ipcTypes.toMain.settings.watcher.getpolling,
SettingsWatcherPollingGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.watcher.setpolling,
SettingsWatcherPollingSet,
);
ipcMain.handle(ipcTypes.toMain.settings.getPpcFilePath, SettingsPpcFilePathGet);
ipcMain.handle(ipcTypes.toMain.settings.setPpcFilePath, SettingsPpcFilePathSet);
ipcMain.handle(
ipcTypes.toMain.settings.getEmsOutFilePath,
SettingEmsOutFilePathGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.setEmsOutFilePath,
SettingEmsOutFilePathSet,
);
// Paint Scale Input Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.getInputConfigs,
SettingsPaintScaleInputConfigsGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setInputConfigs,
SettingsPaintScaleInputConfigsSet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setInputPath,
SettingsPaintScaleInputPathSet,
);
// Paint Scale Output Settings Handlers
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.getOutputConfigs,
SettingsPaintScaleOutputConfigsGet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setOutputConfigs,
SettingsPaintScaleOutputConfigsSet,
);
ipcMain.handle(
ipcTypes.toMain.settings.paintScale.setOutputPath,
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");
});
// Watcher Handlers
ipcMain.on(ipcTypes.toMain.watcher.start, () => {
StartWatcher().catch((error) => {
log.error("Error starting watcher:", error);
});
});
ipcMain.on(ipcTypes.toMain.watcher.stop, () => {
StopWatcher().catch((error) => {
log.error("Error stopping watcher:", error);
});
});
ipcMain.on(ipcTypes.toMain.updates.download, () => {
log.info("Download update requested from renderer.");
autoUpdater.downloadUpdate().catch((error) => {
log.error("Error downloading update:", error);
});
});
export { initializeCronTasks };
logIpcMessages();

View File

@@ -0,0 +1,67 @@
export interface User {
stsTokenManager?: {
accessToken: string;
};
}
export interface BodyShop {
shopname: string;
id: string;
}
export interface GraphQLResponse {
bodyshops_by_pk?: {
imexshopid: string;
shopname: string;
};
jobs?: Array<{
labhrs: any;
larhrs: any;
ro_number: string;
ownr_ln: string;
ownr_fn: string;
plate_no: string;
v_vin: string;
v_model_yr: string;
v_make_desc: string;
v_model_desc: string;
vehicle?: {
v_paint_codes?: {
paint_cd1: string;
};
};
larhrs_aggregate?: {
aggregate?: {
sum?: {
mod_lb_hrs: number;
};
};
};
ins_co_nm: string;
est_ct_ln: string;
est_ct_fn: string;
job_totals?: {
rates?: {
mapa?: {
total?: {
amount: number;
};
};
};
totals?: {
subtotal?: {
amount: number;
};
};
};
rate_mapa: number;
labhrs_aggregate?: {
aggregate?: {
sum?: {
mod_lb_hrs: number;
};
};
};
rate_lab: number;
}>;
}

View File

@@ -0,0 +1,257 @@
// main/ipcMainHandler.settings.ts
import { dialog, IpcMainInvokeEvent } from "electron";
import log from "electron-log/main";
import _ from "lodash";
import Store from "../store/store";
import { getMainWindow } from "../util/toRenderer";
import {
addWatcherPath,
removeWatcherPath,
StartWatcher,
StopWatcher,
} from "../watcher/watcher";
import { PaintScaleConfig } from "../../util/types/paintScale";
// Initialize paint scale input configs in store if not set
if (!Store.get("settings.paintScaleInputConfigs")) {
Store.set("settings.paintScaleInputConfigs", []);
}
// Initialize paint scale output configs in store if not set
if (!Store.get("settings.paintScaleOutputConfigs")) {
Store.set("settings.paintScaleOutputConfigs", []);
}
const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
const mainWindow = getMainWindow();
if (!mainWindow) {
log.error("No main window found when trying to open dialog");
return [];
}
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (!result.canceled) {
Store.set(
"settings.filepaths",
_.union(result.filePaths, Store.get("settings.filepaths")),
);
addWatcherPath(result.filePaths);
}
return Store.get("settings.filepaths");
};
const SettingsWatchedFilePathsRemove = async (
_event: IpcMainInvokeEvent,
path: string,
): Promise<string[]> => {
Store.set(
"settings.filepaths",
_.without(Store.get("settings.filepaths"), path),
);
removeWatcherPath(path);
return Store.get("settings.filepaths");
};
const SettingsWatchedFilePathsGet = async (): Promise<string[]> => {
return Store.get("settings.filepaths") || [];
};
const SettingsWatcherPollingGet = async (): Promise<{
enabled: boolean;
interval: number;
}> => {
const pollingEnabled: { enabled: boolean; interval: number } =
Store.get("settings.polling");
return { enabled: pollingEnabled.enabled, interval: pollingEnabled.interval };
};
const SettingsWatcherPollingSet = async (
_event: IpcMainInvokeEvent,
pollingSettings: {
enabled: boolean;
interval: number;
},
): Promise<{
enabled: boolean;
interval: number;
}> => {
log.info("Polling set", pollingSettings);
const { enabled, interval } = pollingSettings;
Store.set("settings.polling", { enabled, interval });
await StopWatcher();
await StartWatcher();
return { enabled, interval };
};
const SettingsPpcFilePathGet = async (): Promise<string> => {
return Store.get("settings.ppcFilePath");
};
const SettingsPpcFilePathSet = async (): Promise<string> => {
const mainWindow = getMainWindow();
if (!mainWindow) {
log.error("No main window found when trying to open dialog");
return "";
}
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (!result.canceled) {
Store.set("settings.ppcFilePath", result.filePaths[0]);
}
return (Store.get("settings.ppcFilePath") as string) || "";
};
const SettingEmsOutFilePathGet = async (): Promise<string> => {
return Store.get("settings.emsOutFilePath");
};
const SettingEmsOutFilePathSet = async (): Promise<string> => {
const mainWindow = getMainWindow();
if (!mainWindow) {
log.error("No main window found when trying to open dialog");
return "";
}
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (!result.canceled) {
Store.set("settings.emsOutFilePath", result.filePaths[0]);
}
return (Store.get("settings.emsOutFilePath") as string) || "";
};
const SettingsPaintScaleInputConfigsGet = (
_event?: IpcMainInvokeEvent,
): PaintScaleConfig[] => {
try {
const configs = Store.get(
"settings.paintScaleInputConfigs",
) as PaintScaleConfig[];
log.debug("Retrieved paint scale input configs:", configs);
return configs || [];
} catch (error) {
log.error("Error getting paint scale input configs:", error);
throw error;
}
};
const SettingsPaintScaleInputConfigsSet = async (
_event: IpcMainInvokeEvent,
configs: PaintScaleConfig[],
): Promise<boolean> => {
try {
Store.set("settings.paintScaleInputConfigs", configs);
log.debug("Saved paint scale input configs:", configs);
return true;
} catch (error) {
log.error("Error setting paint scale input configs:", error);
throw error;
}
};
const SettingsPaintScaleInputPathSet = async (
_event: IpcMainInvokeEvent,
): Promise<string | null> => {
try {
const mainWindow = getMainWindow();
if (!mainWindow) {
log.error("No main window found when trying to open dialog");
return null;
}
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (result.canceled) {
log.debug("Paint scale input path selection canceled");
return null;
}
const path = result.filePaths[0];
log.debug("Selected paint scale input path:", path);
return path;
} catch (error) {
log.error("Error setting paint scale input path:", error);
throw error;
}
};
const SettingsPaintScaleOutputConfigsGet = (
_event?: IpcMainInvokeEvent,
): PaintScaleConfig[] => {
try {
const configs = Store.get(
"settings.paintScaleOutputConfigs",
) as PaintScaleConfig[];
log.debug("Retrieved paint scale output configs:", configs);
return configs || [];
} catch (error) {
log.error("Error getting paint scale output configs:", error);
throw error;
}
};
const SettingsPaintScaleOutputConfigsSet = async (
_event: IpcMainInvokeEvent,
configs: PaintScaleConfig[],
): Promise<boolean> => {
try {
Store.set("settings.paintScaleOutputConfigs", configs);
log.debug("Saved paint scale output configs:", configs);
return true;
} catch (error) {
log.error("Error setting paint scale output configs:", error);
throw error;
}
};
const SettingsPaintScaleOutputPathSet = async (
_event: IpcMainInvokeEvent,
): Promise<string | null> => {
try {
const mainWindow = getMainWindow();
if (!mainWindow) {
log.error("No main window found when trying to open dialog");
return null;
}
const result = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (result.canceled) {
log.debug("Paint scale output path selection canceled");
return null;
}
const path = result.filePaths[0];
log.debug("Selected paint scale output path:", path);
return path;
} catch (error) {
log.error("Error setting paint scale output path:", error);
throw error;
}
};
export {
SettingsPpcFilePathGet,
SettingsPpcFilePathSet,
SettingsWatchedFilePathsAdd,
SettingsWatchedFilePathsGet,
SettingsWatchedFilePathsRemove,
SettingsWatcherPollingGet,
SettingsWatcherPollingSet,
SettingEmsOutFilePathGet,
SettingEmsOutFilePathSet,
SettingsPaintScaleInputConfigsGet,
SettingsPaintScaleInputConfigsSet,
SettingsPaintScaleInputPathSet,
SettingsPaintScaleOutputConfigsGet,
SettingsPaintScaleOutputConfigsSet,
SettingsPaintScaleOutputPathSet,
};

View File

@@ -0,0 +1,102 @@
import { IpcMainEvent, shell } from "electron";
import log from "electron-log/main";
import { autoUpdater } from "electron-updater";
import { User } from "firebase/auth";
import errorTypeCheck from "../../util/errorTypeCheck";
import ipcTypes from "../../util/ipcTypes.json";
import client from "../graphql/graphql-client";
import {
ActiveBodyshopQueryResult,
MasterdataQueryResult,
QUERY_ACTIVE_BODYSHOP_TYPED,
QUERY_MASTERDATA_TYPED,
} from "../graphql/queries";
import { default as Store, default as store } from "../store/store";
import { checkForAppUpdatesContinuously } from "../util/checkForAppUpdates";
import { getMainWindow, sendIpcToRenderer } from "../util/toRenderer";
const ipcMainHandleAuthStateChanged = async (
_event: IpcMainEvent,
user: User | null,
): Promise<void> => {
Store.set("user", user);
log.debug("Received authentication state change from Renderer.", user);
await setReleaseChannel();
checkForAppUpdatesContinuously();
};
async function setReleaseChannel() {
try {
//Need to query the currently active shop, and store the metadata as well.
//Also need to query the OP Codes for decoding reference.
await handleShopMetaDataFetch();
//Check for updates
const bodyshop = Store.get("app.bodyshop");
if (bodyshop?.convenient_company?.toLowerCase() === "alpha") {
autoUpdater.channel = "alpha";
log.debug("Setting update channel to ALPHA channel.");
} else if (bodyshop?.convenient_company?.toLowerCase() === "beta") {
autoUpdater.channel = "beta";
log.debug("Setting update channel to BETA channel.");
} else {
log.debug("Setting update channel to LATEST channel.");
}
} catch (error) {
log.error(
"Error while querying active bodyshop or master data",
errorTypeCheck(error),
);
sendIpcToRenderer(
ipcTypes.toRenderer.general.showErrorMessage,
"Error connecting to ImEX Online servers to get shop data. Please try again.",
);
}
}
const handleShopMetaDataFetch = async (
reloadWindow?: boolean,
): Promise<void> => {
try {
log.debug("Requery shop information & master data.");
const activeBodyshop: ActiveBodyshopQueryResult = await client.request(
QUERY_ACTIVE_BODYSHOP_TYPED,
);
Store.set("app.bodyshop", activeBodyshop.bodyshops[0]);
const OpCodes: MasterdataQueryResult = await client.request(
QUERY_MASTERDATA_TYPED,
{
key: `${activeBodyshop.bodyshops[0].region_config}_ciecaopcodes`,
},
);
Store.set(
"app.masterdata.opcodes",
JSON.parse(OpCodes.masterdata[0]?.value),
);
if (reloadWindow) {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.reload();
}
}
} catch (error) {
log.error("Error while fetching shop metadata", errorTypeCheck(error));
throw error;
}
};
const ipMainHandleResetPassword = async (): Promise<void> => {
shell.openExternal(
store.get("app.isTest")
? `${import.meta.env.VITE_FE_URL_TEST}/resetpassword`
: `${import.meta.env.VITE_FE_URL}/resetpassword`,
);
};
export {
handleShopMetaDataFetch,
ipcMainHandleAuthStateChanged,
ipMainHandleResetPassword,
setReleaseChannel,
};

View File

@@ -0,0 +1,272 @@
import log from "electron-log/main";
import path from "path";
import fs from "fs/promises";
import axios from "axios";
import { create } from "xmlbuilder2";
import { parseStringPromise } from "xml2js";
import store from "../../store/store";
import client, { getTokenFromRenderer } from "../../graphql/graphql-client";
import { PaintScaleConfig } from "../../../util/types/paintScale";
import dayjs from "dayjs";
import {
PPG_DATA_QUERY_TYPED,
PpgDataQueryResult,
PpgDataQueryVariables,
} from "../../graphql/queries";
export async function ppgInputHandler(config: PaintScaleConfig): Promise<void> {
try {
log.info(
`Polling input directory for PPG config ${config.id}: ${config.path}`,
);
log.debug(
`Archive dir: ${path.join(config.path!, "archive")}, Error dir: ${path.join(config.path!, "error")}`,
);
// Ensure archive and error directories exist
const archiveDir = path.join(config.path!, "archive");
const errorDir = path.join(config.path!, "error");
try {
await fs.mkdir(archiveDir, { recursive: true });
await fs.mkdir(errorDir, { recursive: true });
log.debug(
`Archive and error directories ensured: ${archiveDir}, ${errorDir}`,
);
} catch (dirError) {
log.error(`Failed to create directories for ${config.path}:`, dirError);
throw dirError;
}
// Check for files
const files = await fs.readdir(config.path!);
log.debug(`Found ${files.length} files in ${config.path}:`, files);
for (const file of files) {
// Only process XML files
if (!file.toLowerCase().endsWith(".xml")) {
continue;
}
const filePath = path.join(config.path!, file);
try {
const stats = await fs.stat(filePath);
if (!stats.isFile()) {
continue;
}
} catch (statError) {
log.warn(`Failed to stat file ${filePath}:`, statError);
continue;
}
log.debug(`Processing input file: ${filePath}`);
// Check file accessibility (e.g., not locked)
try {
await fs.access(filePath, fs.constants.R_OK);
} catch (error) {
log.warn(`File ${filePath} is inaccessible, skipping:`, error);
continue;
}
// Validate XML structure
let xmlContent: BlobPart;
try {
xmlContent = await fs.readFile(filePath, "utf8");
await parseStringPromise(xmlContent);
log.debug(`Successfully validated XML for ${filePath}`);
} catch (error) {
log.error(`Invalid XML in ${filePath}:`, error);
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
const originalFilename = path.basename(file, path.extname(file));
const errorPath = path.join(
errorDir,
`${originalFilename}-${timestamp}.xml`,
);
try {
await fs.rename(filePath, errorPath);
log.debug(`Moved invalid file to error: ${errorPath}`);
} catch (moveError) {
log.error(
`Failed to move invalid file to error directory ${errorPath}:`,
moveError,
);
}
continue;
}
// Get authentication token
let token: string | null;
try {
token = await getTokenFromRenderer();
if (!token) {
log.error(`No authentication token for file: ${filePath}`);
continue;
}
log.debug(
`Obtained authentication token for ${filePath}: ${token.slice(0, 10)}...`,
);
} catch (tokenError) {
log.error(
`Failed to obtain authentication token for ${filePath}:`,
tokenError,
);
continue;
}
// Upload file to API
const formData = new FormData();
formData.append("file", new Blob([xmlContent]), path.basename(filePath));
const shopId = (store.get("app.bodyshop") as any)?.shopname || "";
formData.append("shopId", shopId);
log.debug(`Shop ID: ${shopId}`);
const baseURL = store.get("app.isTest")
? import.meta.env.VITE_API_TEST_URL
: import.meta.env.VITE_API_URL;
const finalUrl = `${baseURL}/mixdata/upload`;
log.debug(`Uploading file to ${finalUrl}`);
try {
const response = await axios.post(finalUrl, formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
timeout: 10000, // 10-second timeout
});
log.info(`Upload response for ${filePath}:`, {
status: response.status,
statusText: response.statusText,
data: response.data,
});
if (response.status === 200) {
log.info(`Successful upload of ${filePath}`);
// Move file to archive
const timestamp = dayjs().format("YYYYMMDD_HHmmss");
const originalFilename = path.basename(file, path.extname(file));
const archivePath = path.join(
archiveDir,
`${originalFilename}-${timestamp}.xml`,
);
try {
await fs.access(archiveDir, fs.constants.W_OK); // Verify archiveDir is writable
await fs.rename(filePath, archivePath);
log.info(`Moved file to archive: ${archivePath}`);
} catch (moveError) {
log.error(
`Failed to move file to archive directory ${archivePath}:`,
moveError,
);
}
} else {
log.error(
`Failed to upload ${filePath}: ${response.status} ${response.statusText}`,
{ responseData: response.data },
);
}
} catch (error: any) {
log.error(`Error uploading ${filePath}:`, {
message: error.message,
code: error.code,
response: error.response
? {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data,
}
: null,
});
}
}
} catch (error) {
log.error(`Error polling input directory ${config.path}:`, error);
}
}
// PPG Output Handler
export async function ppgOutputHandler(
config: PaintScaleConfig,
): Promise<void> {
try {
log.info(`Generating PPG output for config ${config.id}: ${config.path}`);
await fs.mkdir(config.path!, { recursive: true });
const variables: PpgDataQueryVariables = {
today: dayjs().toISOString(),
todayplus5: dayjs().add(5, "day").toISOString(),
shopid: (store.get("app.bodyshop") as any)?.id,
};
const response = await client.request<
PpgDataQueryResult,
PpgDataQueryVariables
>(PPG_DATA_QUERY_TYPED, variables);
const jobs = response.jobs ?? [];
const header = {
PPG: {
Header: {
Protocol: {
Message: "PaintShopInterface",
Name: "PPG",
Version: "1.5.0",
},
Transaction: {
TransactionID: "",
TransactionDate: dayjs().format("YYYY-MM-DD:HH:mm"),
},
Product: {
Name: import.meta.env.VITE_COMPANY === "IMEX",
Version: "",
},
},
DataInterface: {
ROData: {
ShopInfo: {
ShopID: response.bodyshops_by_pk?.imexshopid || "",
ShopName: response.bodyshops_by_pk?.shopname || "",
},
RepairOrders: {
ROCount: jobs.length.toString(),
RO: jobs.map((job) => ({
RONumber: job.ro_number || "",
ROStatus: "Open",
Customer: `${job.ownr_ln || ""}, ${job.ownr_fn || ""}`,
ROPainterNotes: "",
LicensePlateNum: job.plate_no || "",
VIN: job.v_vin || "",
ModelYear: job.v_model_yr || "",
MakeDesc: job.v_make_desc || "",
ModelName: job.v_model_desc || "",
OEMColorCode: job.vehicle?.v_paint_codes?.paint_cd1 || "",
RefinishLaborHours: job.larhrs?.aggregate?.sum?.mod_lb_hrs || 0,
InsuranceCompanyName: job.ins_co_nm || "",
EstimatorName: `${job.est_ct_ln || ""}, ${job.est_ct_fn || ""}`,
PaintMaterialsRevenue: (
(job.job_totals?.rates?.mapa?.total?.amount || 0) / 100
).toFixed(2),
PaintMaterialsRate: job.rate_mapa || 0,
BodyHours: job.labhrs?.aggregate?.sum?.mod_lb_hrs || 0,
BodyLaborRate: job.rate_lab || 0,
TotalCostOfRepairs: (
(job.job_totals?.totals?.subtotal?.amount || 0) / 100
).toFixed(2),
})),
},
},
},
},
};
const xml = create({ version: "1.0" }, header).end({ prettyPrint: true });
const outputPath = path.join(config.path!, `PPGPaint.xml`);
await fs.writeFile(outputPath, xml);
log.info(`Saved PPG output XML to ${outputPath}`);
} catch (error) {
log.error(`Error generating PPG output for config ${config.id}:`, error);
}
}

View File

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,67 @@
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

@@ -0,0 +1,30 @@
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

@@ -0,0 +1,130 @@
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

@@ -0,0 +1,77 @@
import { promises as fs } from "fs";
import { join } from "path";
import { homedir } from "os";
import { exec } from "child_process";
import { promisify } from "util";
import log from "electron-log/main";
const execPromise = promisify(exec);
// Define the interval as a variable (in seconds)
const KEEP_ALIVE_INTERVAL_SECONDS = 15 * 60; // 15 minutes
export async function setupKeepAliveAgent(): Promise<void> {
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.convenientbrands.bodyshop-desktop.keepalive</string>
<key>ProgramArguments</key>
<array>
<string>Shop Partner Keep Alive</string>
<string>imexmedia://keep-alive</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>${KEEP_ALIVE_INTERVAL_SECONDS}</integer>
</dict>
</plist>`;
const plistPath = join(
homedir(),
"/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
);
try {
await fs.writeFile(plistPath, plistContent);
const { stdout, stderr } = await execPromise(`launchctl load ${plistPath}`);
log.info(`Launch agent created and loaded: ${stdout}`);
if (stderr) log.warn(`Launch agent stderr: ${stderr}`);
} catch (error) {
log.error(
`Error setting up launch agent: ${error instanceof Error ? error.message : String(error)}`,
);
throw error; // Rethrow to allow caller to handle
}
}
export async function isKeepAliveAgentInstalled(): Promise<boolean> {
const plistPath = join(
homedir(),
"/Library/LaunchAgents/com.convenientbrands.bodyshop-desktop.keepalive.plist",
);
const maxRetries = 3;
const retryDelay = 500; // 500ms delay between retries
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await fs.access(plistPath, fs.constants.F_OK);
const { stdout } = await execPromise(
`launchctl list | grep com.convenientbrands.bodyshop-desktop.keepalive`,
);
return !!stdout; // Return true if plist exists and agent is loaded
} catch (error) {
log.debug(
`Launch agent not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`,
);
if (attempt === maxRetries) {
return false; // Return false after all retries fail
}
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
return false; // Fallback return
}

View File

@@ -0,0 +1,52 @@
import { exec } from "child_process";
import { promisify } from "util";
import log from "electron-log/main";
const execPromise = promisify(exec);
// Define the interval as a variable (in minutes)
const KEEP_ALIVE_INTERVAL_MINUTES = 15;
const taskName = "ShopPartnerKeepAlive";
export async function setupKeepAliveTask(): Promise<void> {
const protocolUrl = "imexmedia://keep-alive";
// Use rundll32.exe to silently open the URL as a protocol
const command = `rundll32.exe url.dll,OpenURL "${protocolUrl}"`;
// Escape quotes for schtasks /tr parameter
const escapedCommand = command.replace(/"/g, '\\"');
const schtasksCommand = `schtasks /create /tn "${taskName}" /tr "${escapedCommand}" /sc minute /mo ${KEEP_ALIVE_INTERVAL_MINUTES} /f`;
try {
const { stdout, stderr } = await execPromise(schtasksCommand);
log.info(`Scheduled task created: ${stdout}`);
if (stderr) log.warn(`Scheduled task stderr: ${stderr}`);
} catch (error) {
log.error(
`Error creating scheduled task: ${error instanceof Error ? error.message : String(error)}`,
);
throw error; // Rethrow to allow caller to handle
}
}
export async function isKeepAliveTaskInstalled(): Promise<boolean> {
const maxRetries = 3;
const retryDelay = 500; // 500ms delay between retries
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const { stdout } = await execPromise(`schtasks /query /tn "${taskName}"`);
return !!stdout; // Return true if task exists
} catch (error) {
log.debug(
`Scheduled task ${taskName} not found (attempt ${attempt}/${maxRetries}): ${error instanceof Error ? error.message : String(error)}`,
);
if (attempt === maxRetries) {
return false; // Return false after all retries fail
}
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
return false; // Fallback return
}

40
src/main/store/store.ts Normal file
View File

@@ -0,0 +1,40 @@
import Store from "electron-store";
const store = new Store({
defaults: {
settings: {
runOnStartup: true,
filepaths: [],
ppcFilePath: null,
emsOutFilePath: null,
qbFilePath: "",
runWatcherOnStartup: true,
enableMemDebug: false,
polling: {
enabled: false,
interval: 30000,
},
},
app: {
windowBounds: {
width: 800,
height: 600,
x: undefined,
y: undefined,
},
user: null,
isTest: false,
bodyshop: {},
masterdata: {
opcodes: null,
},
},
},
});
// store.onDidAnyChange((newValue, oldValue) => {
// const mainWindow = BrowserWindow.getAllWindows()[0];
// mainWindow?.webContents.send(ipcTypes.toRenderer.store.didChange, newValue);
// });
export default store;

View File

@@ -0,0 +1,23 @@
import { autoUpdater } from "electron-updater";
import { setReleaseChannel } from "../ipc/ipcMainHandler.user";
let continuousUpdatesTriggered = false;
async function checkForAppUpdatesContinuously(): Promise<void> {
if (!continuousUpdatesTriggered) {
continuousUpdatesTriggered = true;
checkForAppUpdates();
setInterval(
() => {
checkForAppUpdates();
},
1000 * 60 * 30,
);
}
}
async function checkForAppUpdates(): Promise<void> {
await setReleaseChannel();
autoUpdater.checkForUpdates();
}
export { checkForAppUpdates, checkForAppUpdatesContinuously };

View File

@@ -0,0 +1,21 @@
import log from "electron-log/main";
import fs from "fs";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
const createdDirectoryIfNotExist = async (dirPath: string) => {
try {
const directoryPath = path.dirname(dirPath);
if (!fs.existsSync(directoryPath)) {
log.info(`Directory does not exist. Creating: ${directoryPath}`);
fs.mkdirSync(directoryPath, { recursive: true });
}
} catch (error) {
log.error("Error creating directory as needed", errorTypeCheck(error));
throw new Error(
"Error creating directory: " + errorTypeCheck(error).message,
);
}
};
export default createdDirectoryIfNotExist;

View File

@@ -0,0 +1,706 @@
import { FieldDescriptor } from "dbffile";
export const ad1FieldLineDescriptors: FieldDescriptor[] = [
{
name: "INS_CO_ID",
type: "C",
size: 5,
decimalPlaces: 0,
},
{
name: "INS_CO_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INS_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "INS_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "INS_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "INS_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "INS_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "INS_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "INS_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INS_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INS_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INS_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INS_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INS_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INS_CT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INS_CT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INS_TITLE",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INS_CT_PH",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INS_CT_PHX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INS_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "INS_MEMO",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "POLICY_NO",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "DED_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "DED_STATUS",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "ASGN_NO",
type: "C",
size: 25,
decimalPlaces: 0,
},
{
name: "ASGN_DATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "ASGN_TYPE",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "CLM_NO",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "CLM_OFC_ID",
type: "C",
size: 5,
decimalPlaces: 0,
},
{
name: "CLM_OFC_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "CLM_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "CLM_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "CLM_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "CLM_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "CLM_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "CLM_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "CLM_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CLM_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "CLM_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CLM_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "CLM_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CLM_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "CLM_CT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "CLM_CT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "CLM_TITLE",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "CLM_CT_PH",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CLM_CT_PHX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "CLM_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "PAYEE_NMS",
type: "C",
size: 85,
decimalPlaces: 0,
},
{
name: "PAY_TYPE",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "PAY_DATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "PAY_CHKNM",
type: "C",
size: 16,
decimalPlaces: 0,
},
{
name: "PAY_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "PAY_MEMO",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "AGT_CO_ID",
type: "C",
size: 5,
decimalPlaces: 0,
},
{
name: "AGT_CO_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "AGT_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "AGT_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "AGT_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "AGT_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "AGT_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "AGT_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "AGT_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "AGT_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "AGT_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "AGT_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "AGT_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "AGT_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "AGT_CT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "AGT_CT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "AGT_CT_PH",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "AGT_CT_PHX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "AGT_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "AGT_LIC_NO",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "LOSS_DATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "LOSS_CAT",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "LOSS_TYPE",
type: "C",
size: 7,
decimalPlaces: 0,
},
{
name: "LOSS_DESC",
type: "C",
size: 38,
decimalPlaces: 0,
},
{
name: "THEFT_IND",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "CAT_NO",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "TLOS_IND",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LOSS_MEMO",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CUST_PR",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "INSD_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INSD_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INSD_TITLE",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "INSD_CO_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INSD_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "INSD_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "INSD_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "INSD_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "INSD_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "INSD_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "INSD_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INSD_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INSD_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INSD_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INSD_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INSD_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INSD_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "OWNR_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "OWNR_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "OWNR_TITLE",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "OWNR_CO_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "OWNR_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "OWNR_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "OWNR_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "OWNR_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "OWNR_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "OWNR_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "OWNR_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "OWNR_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "OWNR_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "OWNR_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "OWNR_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "OWNR_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "OWNR_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,640 @@
import { FieldDescriptor } from "dbffile";
export const ad2FieldLineDescriptors: FieldDescriptor[] = [
{
name: "CLMT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "CLMT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "CLMT_TITLE",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "CLMT_CO_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "CLMT_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "CLMT_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "CLMT_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "CLMT_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "CLMT_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "CLMT_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "CLMT_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CLMT_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "CLMT_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CLMT_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "CLMT_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "CLMT_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "CLMT_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "EST_CO_ID",
type: "C",
size: 5,
decimalPlaces: 0,
},
{
name: "EST_CO_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "EST_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "EST_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "EST_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "EST_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "EST_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "EST_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "EST_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "EST_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "EST_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "EST_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "EST_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "EST_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "EST_CT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "EST_CT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "EST_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "EST_LIC_NO",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "EST_FILENO",
type: "C",
size: 25,
decimalPlaces: 0,
},
{
name: "INSP_CT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INSP_CT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "INSP_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "INSP_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "INSP_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "INSP_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "INSP_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "INSP_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "INSP_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INSP_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INSP_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INSP_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INSP_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "INSP_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "INSP_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "INSP_CODE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "INSP_DESC",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "INSP_DATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "INSP_TIME",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "RF_CO_ID",
type: "C",
size: 5,
decimalPlaces: 0,
},
{
name: "RF_CO_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "RF_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "RF_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "RF_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "RF_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "RF_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "RF_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "RF_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "RF_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "RF_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "RF_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "RF_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "RF_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "RF_CT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "RF_CT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "RF_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "RF_TAX_ID",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "RF_LIC_NO",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "RF_BAR_NO",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "RO_IN_DATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "RO_IN_TIME",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "RO_AUTH",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "TAR_DATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "TAR_TIME",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "RO_CMPDATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "RO_CMPTIME",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "DATE_OUT",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "TIME_OUT",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "RF_ESTIMTR",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "MKTG_TYPE",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "MKTG_SRC",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "LOC_NM",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "LOC_ADDR1",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "LOC_ADDR2",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "LOC_CITY",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "LOC_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LOC_ZIP",
type: "C",
size: 11,
decimalPlaces: 0,
},
{
name: "LOC_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "LOC_PH1",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "LOC_PH1X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "LOC_PH2",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "LOC_PH2X",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "LOC_FAX",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "LOC_FAXX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "LOC_CT_LN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "LOC_CT_FN",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "LOC_TITLE",
type: "C",
size: 35,
decimalPlaces: 0,
},
{
name: "LOC_PH",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "LOC_PHX",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "LOC_EA",
type: "C",
size: 80,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,154 @@
import { FieldDescriptor } from "dbffile";
export const envFieldLineDescriptors: FieldDescriptor[] = [
{
name: "EST_SYSTEM",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "SW_VERSION",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "DB_VERSION",
type: "C",
size: 12,
decimalPlaces: 0,
},
{
name: "DB_DATE",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "UNQFILE_ID",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "RO_ID",
type: "C",
size: 8,
decimalPlaces: 0,
},
{
name: "ESTFILE_ID",
type: "C",
size: 38,
decimalPlaces: 0,
},
{
name: "SUPP_NO",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "EST_CTRY",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "TOP_SECRET",
type: "C",
size: 80,
decimalPlaces: 0,
},
{
name: "H_TRANS_ID",
type: "C",
size: 9,
decimalPlaces: 0,
},
{
name: "H_CTRL_NO",
type: "C",
size: 9,
decimalPlaces: 0,
},
{
name: "TRANS_TYPE",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "STATUS",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "CREATE_DT",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "CREATE_TM",
type: "C",
size: 6,
decimalPlaces: 0,
},
{
name: "TRANSMT_DT",
type: "D",
size: 8,
decimalPlaces: 0,
},
{
name: "TRANSMT_TM",
type: "C",
size: 6,
decimalPlaces: 0,
},
{
name: "INCL_ADMIN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "INCL_VEH",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "INCL_EST",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "INCL_PROFL",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "INCL_TOTAL",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "INCL_VENDR",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "EMS_VER",
type: "C",
size: 5,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,274 @@
import { FieldDescriptor } from "dbffile";
export const linFieldDescriptors: FieldDescriptor[] = [
{
name: "LINE_NO",
type: "N",
size: 3,
decimalPlaces: 0,
},
{
name: "LINE_IND",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "LINE_REF",
type: "N",
size: 3,
decimalPlaces: 0,
},
{
name: "TRAN_CODE",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "DB_REF",
type: "C",
size: 7,
decimalPlaces: 0,
},
{
name: "UNQ_SEQ",
type: "N",
size: 4,
decimalPlaces: 0,
},
{
name: "WHO_PAYS",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LINE_DESC",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "PART_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "PART_DES_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "GLASS_FLAG",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "OEM_PARTNO",
type: "C",
size: 25,
decimalPlaces: 0,
},
{
name: "PRICE_INC",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "ALT_PART_I",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "TAX_PART",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "DB_PRICE",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "ACT_PRICE",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "PRICE_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "CERT_PART",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PART_QTY",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "ALT_CO_ID",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "ALT_PARTNO",
type: "C",
size: 25,
decimalPlaces: 0,
},
{
name: "ALT_OVERRD",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "ALT_PARTM",
type: "C",
size: 45,
decimalPlaces: 0,
},
{
name: "PRT_DSMK_P",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "PRT_DSMK_M",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "MOD_LBR_TY",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "DB_HRS",
type: "N",
size: 5,
decimalPlaces: 1,
},
{
name: "MOD_LB_HRS",
type: "N",
size: 5,
decimalPlaces: 1,
},
{
name: "LBR_INC",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_OP",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "LBR_HRS_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TYP_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_OP_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PAINT_STG",
type: "N",
size: 1,
decimalPlaces: 0,
},
{
name: "PAINT_TONE",
type: "N",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TAX",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "MISC_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "MISC_SUBLT",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "MISC_TAX",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "BETT_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "BETT_PCTG",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "BETT_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "BETT_TAX",
type: "L",
size: 1,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,274 @@
import { FieldDescriptor } from "dbffile";
export const linFieldLineDescriptors: FieldDescriptor[] = [
{
name: "LINE_NO",
type: "N",
size: 3,
decimalPlaces: 0,
},
{
name: "LINE_IND",
type: "C",
size: 3,
decimalPlaces: 0,
},
{
name: "LINE_REF",
type: "N",
size: 3,
decimalPlaces: 0,
},
{
name: "TRAN_CODE",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "DB_REF",
type: "C",
size: 7,
decimalPlaces: 0,
},
{
name: "UNQ_SEQ",
type: "N",
size: 4,
decimalPlaces: 0,
},
{
name: "WHO_PAYS",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LINE_DESC",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "PART_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "PART_DES_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "GLASS_FLAG",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "OEM_PARTNO",
type: "C",
size: 25,
decimalPlaces: 0,
},
{
name: "PRICE_INC",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "ALT_PART_I",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "TAX_PART",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "DB_PRICE",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "ACT_PRICE",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "PRICE_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "CERT_PART",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PART_QTY",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "ALT_CO_ID",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "ALT_PARTNO",
type: "C",
size: 25,
decimalPlaces: 0,
},
{
name: "ALT_OVERRD",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "ALT_PARTM",
type: "C",
size: 45,
decimalPlaces: 0,
},
{
name: "PRT_DSMK_P",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "PRT_DSMK_M",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "MOD_LBR_TY",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "DB_HRS",
type: "N",
size: 5,
decimalPlaces: 1,
},
{
name: "MOD_LB_HRS",
type: "N",
size: 5,
decimalPlaces: 1,
},
{
name: "LBR_INC",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_OP",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "LBR_HRS_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TYP_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_OP_J",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PAINT_STG",
type: "N",
size: 1,
decimalPlaces: 0,
},
{
name: "PAINT_TONE",
type: "N",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TAX",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "MISC_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "MISC_SUBLT",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "MISC_TAX",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "BETT_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "BETT_PCTG",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "BETT_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "BETT_TAX",
type: "L",
size: 1,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,118 @@
import { FieldDescriptor } from "dbffile";
export const pfhFieldLineDescriptors: FieldDescriptor[] = [
{
name: "ID_PRO_NAM",
type: "C",
size: 40,
decimalPlaces: 0,
},
{
name: "TAX_PRETHR",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TAX_THRAMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "TAX_PSTTHR",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TAX_TOW_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "TAX_TOW_RT",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TAX_STR_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "TAX_STR_RT",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TAX_SUB_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "TAX_SUB_RT",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TAX_BTR_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "TAX_LBR_RT",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TAX_GST_RT",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TAX_GST_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "ADJ_G_DISC",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "ADJ_TOWDIS",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "ADJ_STRDIS",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "ADJ_BTR_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "TAX_PREDIS",
type: "N",
size: 7,
decimalPlaces: 2,
},
];

View File

@@ -0,0 +1,100 @@
import { FieldDescriptor } from "dbffile";
export const pflFieldLineDescriptors: FieldDescriptor[] = [
{
name: "LBR_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "LBR_DESC",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "LBR_RATE",
type: "N",
size: 6,
decimalPlaces: 2,
},
{
name: "LBR_TAX_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TAXP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "LBR_ADJP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "LBR_TX_TY1",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LBR_TX_IN1",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TX_TY2",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LBR_TX_IN2",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TX_TY3",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LBR_TX_IN3",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TX_TY4",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LBR_TX_IN4",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "LBR_TX_TY5",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "LBR_TX_IN5",
type: "C",
size: 1,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,166 @@
import { FieldDescriptor } from "dbffile";
export const pfmFieldLineDescriptors: FieldDescriptor[] = [
{
name: "MATL_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "CAL_CODE",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "CAL_DESC",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "CAL_MAXDLR",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "CAL_PRIP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "CAL_SECP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "MAT_CALP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "CAL_PRETHR",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "CAL_PSTTHR",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "CAL_THRAMT",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "CAL_LBRMIN",
type: "N",
size: 4,
decimalPlaces: 1,
},
{
name: "CAL_LBRMAX",
type: "N",
size: 4,
decimalPlaces: 1,
},
{
name: "CAL_LBRRTE",
type: "N",
size: 6,
decimalPlaces: 2,
},
{
name: "CAL_OPCODE",
type: "C",
size: 48,
decimalPlaces: 0,
},
{
name: "TAX_IND",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "MAT_TAXP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "MAT_ADJP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "MAT_TX_TY1",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "MAT_TX_IN1",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "MAT_TX_TY2",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "MAT_TX_IN2",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "MAT_TX_TY3",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "MAT_TX_IN3",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "MAT_TX_TY4",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "MAT_TX_IN4",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "MAT_TX_TY5",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "MAT_TX_IN5",
type: "C",
size: 1,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,160 @@
import { FieldDescriptor } from "dbffile";
export const pfoFieldLineDescriptors: FieldDescriptor[] = [
{
name: "TX_TOW_TY",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "TOW_T_TY1",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TOW_T_IN1",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "TOW_T_TY2",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TOW_T_IN2",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "TOW_T_TY3",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TOW_T_IN3",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "TOW_T_TY4",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TOW_T_IN4",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "TOW_T_TY5",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TOW_T_IN5",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "TOW_T_TY6",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TOW_T_IN6",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "TX_STOR_TY",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "STOR_T_TY1",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "STOR_T_IN1",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "STOR_T_TY2",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "STOR_T_IN2",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "STOR_T_TY3",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "STOR_T_IN3",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "STOR_T_TY4",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "STOR_T_IN4",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "STOR_T_TY5",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "STOR_T_IN5",
type: "C",
size: 1,
decimalPlaces: 0,
},
{
name: "STOR_T_TY6",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "STOR_T_IN6",
type: "C",
size: 1,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,100 @@
import { FieldDescriptor } from "dbffile";
export const pfpFieldLineDescriptors: FieldDescriptor[] = [
{
name: "PRT_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "PRT_TAX_IN",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PRT_TAX_RT",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "PRT_MKUPP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "PRT_MKTYP",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PRT_DISCP",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "PRT_TX_TY1",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "PRT_TX_IN1",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PRT_TX_TY2",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "PRT_TX_IN2",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PRT_TX_TY3",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "PRT_TX_IN3",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PRT_TX_TY4",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "PRT_TX_IN4",
type: "L",
size: 1,
decimalPlaces: 0,
},
{
name: "PRT_TX_TY5",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "PRT_TX_IN5",
type: "L",
size: 1,
decimalPlaces: 0,
},
];

View File

@@ -0,0 +1,760 @@
import { FieldDescriptor } from "dbffile";
export const pftFieldLineDescriptors: FieldDescriptor[] = [
{
name: "TAX_TYPE1",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TY1_TIER1",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY1_THRES1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_RATE1",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY1_SUR1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_TIER2",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY1_THRES2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_RATE2",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY1_SUR2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_TIER3",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY1_THRES3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_RATE3",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY1_SUR3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_TIER4",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY1_THRES4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_RATE4",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY1_SUR4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_TIER5",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY1_THRES5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY1_RATE5",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY1_SUR5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TAX_TYPE2",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TY2_TIER1",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY2_THRES1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_RATE1",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY2_SUR1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_TIER2",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY2_THRES2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_RATE2",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY2_SUR2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_TIER3",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY2_THRES3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_RATE3",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY2_SUR3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_TIER4",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY2_THRES4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_RATE4",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY2_SUR4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_TIER5",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY2_THRES5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY2_RATE5",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY2_SUR5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TAX_TYPE3",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TY3_TIER1",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY3_THRES1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_RATE1",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY3_SUR1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_TIER2",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY3_THRES2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_RATE2",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY3_SUR2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_TIER3",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY3_THRES3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_RATE3",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY3_SUR3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_TIER4",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY3_THRES4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_RATE4",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY3_SUR4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_TIER5",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY3_THRES5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY3_RATE5",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY3_SUR5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TAX_TYPE4",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TY4_TIER1",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY4_THRES1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_RATE1",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY4_SUR1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_TIER2",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY4_THRES2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_RATE2",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY4_SUR2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_TIER3",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY4_THRES3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_RATE3",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY4_SUR3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_TIER4",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY4_THRES4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_RATE4",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY4_SUR4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_TIER5",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY4_THRES5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY4_RATE5",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY4_SUR5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TAX_TYPE5",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TY5_TIER1",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY5_THRES1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_RATE1",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY5_SUR1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_TIER2",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY5_THRES2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_RATE2",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY5_SUR2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_TIER3",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY5_THRES3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_RATE3",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY5_SUR3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_TIER4",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY5_THRES4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_RATE4",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY5_SUR4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_TIER5",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY5_THRES5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY5_RATE5",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY5_SUR5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TAX_TYPE6",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "TY6_TIER1",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY6_THRES1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_RATE1",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY6_SUR1",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_TIER2",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY6_THRES2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_RATE2",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY6_SUR2",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_TIER3",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY6_THRES3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_RATE3",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY6_SUR3",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_TIER4",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY6_THRES4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_RATE4",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY6_SUR4",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_TIER5",
type: "N",
size: 2,
decimalPlaces: 0,
},
{
name: "TY6_THRES5",
type: "N",
size: 7,
decimalPlaces: 2,
},
{
name: "TY6_RATE5",
type: "N",
size: 8,
decimalPlaces: 4,
},
{
name: "TY6_SUR5",
type: "N",
size: 7,
decimalPlaces: 2,
},
];

View File

@@ -0,0 +1,112 @@
import { FieldDescriptor } from "dbffile";
export const stlFieldLineDescriptors: FieldDescriptor[] = [
{
name: "TTL_TYPE",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "TTL_TYPECD",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "T_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "T_HRS",
type: "N",
size: 5,
decimalPlaces: 1,
},
{
name: "T_ADDLBR",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "T_DISCAMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "T_MKUPAMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "T_GDISCAMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "TAX_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "NT_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "NT_HRS",
type: "N",
size: 5,
decimalPlaces: 1,
},
{
name: "NT_ADDLBR",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "NT_DISC",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "NT_MKUP",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "NT_GDIS",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "TTL_TYPAMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
{
name: "TTL_HRS",
type: "N",
size: 5,
decimalPlaces: 1,
},
{
name: "TTL_AMT",
type: "N",
size: 9,
decimalPlaces: 2,
},
];

View File

@@ -0,0 +1,88 @@
import { FieldDescriptor } from "dbffile";
export const ttlFieldLineDescriptors: FieldDescriptor[] = [
{
name: "G_TTL_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_BETT_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_RPD_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_DED_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_CUST_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_AA_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "N_TTL_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "PREV_NET",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "SUPP_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "N_SUPP_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_UPD_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_TTL_DISC",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "G_TAX",
type: "N",
size: 10,
decimalPlaces: 2,
},
{
name: "GST_AMT",
type: "N",
size: 10,
decimalPlaces: 2,
},
];

View File

@@ -0,0 +1,172 @@
import { FieldDescriptor } from "dbffile";
export const vehFieldLineDescriptors: FieldDescriptor[] = [
{
name: "IMPACT_1",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "IMPACT_2",
type: "C",
size: 30,
decimalPlaces: 0,
},
{
name: "DMG_MEMO",
type: "C", // Changed from "M" to "C" to allow writing, need to verify if this still works.
size: 10,
decimalPlaces: 0,
},
{
name: "DB_V_CODE",
type: "C",
size: 7,
decimalPlaces: 0,
},
{
name: "PLATE_NO",
type: "C",
size: 10,
decimalPlaces: 0,
},
{
name: "PLATE_ST",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "V_VIN",
type: "C",
size: 25,
decimalPlaces: 0,
},
{
name: "V_COND",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "V_PROD_DT",
type: "C",
size: 4,
decimalPlaces: 0,
},
{
name: "V_MODEL_YR",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "V_MAKECODE",
type: "C",
size: 12,
decimalPlaces: 0,
},
{
name: "V_MAKEDESC",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "V_MODEL",
type: "C",
size: 50,
decimalPlaces: 0,
},
{
name: "V_TYPE",
type: "C",
size: 2,
decimalPlaces: 0,
},
{
name: "V_BSTYLE",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "V_TRIMCODE",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "TRIM_COLOR",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "V_MLDGCODE",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "V_ENGINE",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "V_MILEAGE",
type: "C",
size: 6,
decimalPlaces: 0,
},
{
name: "V_OPTIONS",
type: "C", // Changed from "M" to "C" to allow writing, need to verify if this still works.
size: 10,
decimalPlaces: 0,
},
{
name: "V_COLOR",
type: "C",
size: 20,
decimalPlaces: 0,
},
{
name: "V_TONE",
type: "N",
size: 1,
decimalPlaces: 0,
},
{
name: "V_STAGE",
type: "N",
size: 1,
decimalPlaces: 0,
},
{
name: "PAINT_CD1",
type: "C",
size: 15,
decimalPlaces: 0,
},
{
name: "PAINT_CD2",
type: "C",
size: 15,
decimalPlaces: 0,
},
{
name: "PAINT_CD3",
type: "C",
size: 15,
decimalPlaces: 0,
},
{
name: "V_MEMO", //dbffile does not support writing to a memo field.
type: "C", // Changed from "M" to "C" to allow writing, need to verify if this still works.
size: 10,
decimalPlaces: 0,
},
];

36
src/main/util/ems-util.ts Normal file
View File

@@ -0,0 +1,36 @@
import path from "path";
import store from "../store/store";
import fs from "fs";
const generatePpcFilePath = (filename: string): string => {
const ppcOutFilePath: string | null = store.get("settings.ppcFilePath");
if (!ppcOutFilePath) {
throw new Error("PPC file path is not set");
}
return path.resolve(ppcOutFilePath, filename);
};
const generateEmsOutFilePath = (filename: string): string => {
const emsOutFilePath: string | null = store.get("settings.emsOutFilePath");
if (!emsOutFilePath) {
throw new Error("EMS Out file path is not set");
}
return path.resolve(emsOutFilePath, filename);
};
const deleteEmsFileIfExists = async (filename: string): Promise<void> => {
// Check if the file exists and delete it if it does
try {
await fs.promises.access(filename); // Check if the file exists
await fs.promises.unlink(filename); // Delete the file
console.log(`Existing file at ${filename} deleted.`);
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
// If the error is not "file not found", rethrow it
throw err;
}
console.log(`No existing file found at ${filename}.`);
}
};
export { generatePpcFilePath, generateEmsOutFilePath, deleteEmsFileIfExists };

View File

@@ -0,0 +1,109 @@
import { screen } from "electron";
function ensureWindowOnScreen(
x: number | undefined,
y: number | undefined,
windowWidth: number,
windowHeight: number,
): { validX: number | undefined; validY: number | undefined } {
// If no coordinates stored, let Electron position window automatically
if (x === undefined || y === undefined) {
return { validX: undefined, validY: undefined };
}
const displays = screen.getAllDisplays();
// Minimum visible pixels required on each edge to be considered "visible enough"
const MIN_VISIBLE = 50; // Ensure at least 50px from each edge is visible
// Try to find a display where the window would be almost fully visible
for (const display of displays) {
const { bounds } = display;
// Check if window is mostly within this display
if (
x + MIN_VISIBLE >= bounds.x &&
x + windowWidth - MIN_VISIBLE <= bounds.x + bounds.width &&
y + MIN_VISIBLE >= bounds.y &&
y + windowHeight - MIN_VISIBLE <= bounds.y + bounds.height
) {
// Window is adequately visible on this display
return { validX: x, validY: y };
}
}
// If window isn't adequately visible on any display, try to adjust it to fit the closest display
const closestDisplay = findClosestDisplay(displays, x, y);
const { bounds } = closestDisplay;
// Adjust position to ensure window is fully on screen
let adjustedX = x;
let adjustedY = y;
// Adjust horizontal position if needed
if (x < bounds.x) {
adjustedX = bounds.x;
} else if (x + windowWidth > bounds.x + bounds.width) {
adjustedX = bounds.x + bounds.width - windowWidth;
}
// Adjust vertical position if needed
if (y < bounds.y) {
adjustedY = bounds.y;
} else if (y + windowHeight > bounds.y + bounds.height) {
adjustedY = bounds.y + bounds.height - windowHeight;
}
// If adjustments keep window on screen, use adjusted position
if (
adjustedX >= bounds.x &&
adjustedX + windowWidth <= bounds.x + bounds.width &&
adjustedY >= bounds.y &&
adjustedY + windowHeight <= bounds.y + bounds.height
) {
return { validX: adjustedX, validY: adjustedY };
}
// If all else fails, center on primary display
const primaryDisplay = screen.getPrimaryDisplay();
const primaryBounds = primaryDisplay.bounds;
return {
validX: Math.floor(
primaryBounds.x + (primaryBounds.width - windowWidth) / 2,
),
validY: Math.floor(
primaryBounds.y + (primaryBounds.height - windowHeight) / 2,
),
};
}
// Helper function to find the closest display to a point
function findClosestDisplay(
displays: Electron.Display[],
x: number,
y: number,
): Electron.Display {
let closestDisplay = displays[0];
let shortestDistance = Number.MAX_VALUE;
for (const display of displays) {
const { bounds } = display;
// Calculate distance to center of display
const displayCenterX = bounds.x + bounds.width / 2;
const displayCenterY = bounds.y + bounds.height / 2;
const distance = Math.sqrt(
Math.pow(x - displayCenterX, 2) + Math.pow(y - displayCenterY, 2),
);
if (distance < shortestDistance) {
shortestDistance = distance;
closestDisplay = display;
}
}
return closestDisplay;
}
export default ensureWindowOnScreen;

View File

@@ -0,0 +1,9 @@
import { getMainWindow } from "./toRenderer";
const setAppProgressbar = (progress: number): void => {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.setProgressBar(progress);
}
};
export default setAppProgressbar;

View File

@@ -0,0 +1,20 @@
import {BrowserWindow} from "electron";
import log from "electron-log/main";
const getMainWindow = (): Electron.BrowserWindow => {
return BrowserWindow.getAllWindows()[0];
};
const sendIpcToRenderer = (ipcMessage: string, ...args: any[]): void => {
const window = getMainWindow();
if (window) {
window.webContents.send(ipcMessage, ...args);
} else {
log.error(
"Unable to find main window. Cannot send IPC message.",
ipcMessage,
args,
);
}
};
export { getMainWindow, sendIpcToRenderer };

View File

@@ -0,0 +1,24 @@
/**
* Converts all keys of an object to uppercase
* @param obj The object whose keys need to be converted to uppercase
* @returns A new object with all keys converted to uppercase
*/
function uppercaseObjectKeys<T extends Record<string, any>>(
obj: T,
): Record<string, any> {
if (typeof obj !== "object" || obj === null) {
return obj;
}
return Object.entries(obj).reduce(
(result, [key, value]) => {
const uppercaseKey = key.toUpperCase();
result[uppercaseKey] = typeof value === "object" && value !== null
? uppercaseObjectKeys(value)
: value;
return result;
},
{} as Record<string, any>,
);
}
export default uppercaseObjectKeys;

170
src/main/watcher/watcher.ts Normal file
View File

@@ -0,0 +1,170 @@
import chokidar, { FSWatcher } from "chokidar";
import { BrowserWindow, Notification } from "electron";
import log from "electron-log/main";
import fs from "fs";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
import ipcTypes from "../../util/ipcTypes.json";
import ImportJob from "../decoder/decoder";
import store from "../store/store";
import getMainWindow from "../../util/getMainWindow";
let watcher: FSWatcher | null;
async function StartWatcher(): Promise<boolean> {
const filePaths: string[] = store.get("settings.filepaths") || [];
if (filePaths.length === 0) {
new Notification({
//TODO: Add Translations
title: "Watcher cannot start",
body: "Please set the appropriate file paths and try again.",
}).show();
log.warn("Cannot start watcher. No file paths set.");
return false;
}
if (watcher) {
try {
log.info("Trying to close watcher - it already existed.");
await watcher.close();
log.info("Watcher closed successfully!");
} catch (error) {
log.error("Error trying to close Watcher.", error);
}
}
const pollingSettings =
(store.get("settings.polling") as {
enabled?: boolean;
interval?: number;
}) || {};
watcher = chokidar.watch(filePaths, {
ignored: (filepath, stats) => {
const p = path.parse(filepath);
return (
(!stats?.isFile() && p.ext !== "" && p.ext.toUpperCase() !== ".ENV") ||
p.name?.toUpperCase() === ".DS_STORE"
); //Only watch for .ENV files.
},
usePolling: pollingSettings.enabled || false,
interval: pollingSettings.interval || 30000,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
pollInterval: 500,
stabilityThreshold: 2000,
},
});
watcher
.on("add", async function (path) {
console.log("File", path, "has been added");
HandleNewFile(path);
})
// .on("addDir", function (path) {
// console.log("Directory", path, "has been added");
// })
.on("change", async function (path) {
console.log("File", path, "has been changed");
HandleNewFile(path);
})
// .on("unlink", function (path) {
// console.log("File", path, "has been removed");
// })
// .on("unlinkDir", function (path) {
// console.log("Directory", path, "has been removed");
// })
.on("error", function (error) {
log.error("Error in Watcher", errorTypeCheck(error));
// mainWindow.webContents.send(
// ipcTypes.toRenderer.watcher.error,
// errorTypeCheck(error)
// );
})
.on("ready", onWatcherReady);
// .on("raw", function (event, path, details) {
// // This event should be triggered everytime something happens.
// // console.log("Raw event info:", event, path, details);
// });
return true;
}
function removeWatcherPath(path: string): void {
if (watcher) {
watcher.unwatch(path);
log.debug(`Stopped watching path: ${path}`);
}
}
function addWatcherPath(path: string | string[]): void {
if (watcher) {
watcher.add(path);
log.debug(`Started watching path: ${path}`);
}
}
function onWatcherReady(): void {
if (watcher) {
const mainWindow = getMainWindow();
new Notification({
title: "Watcher Started",
body: "Newly exported estimates will be automatically uploaded.",
}).show();
log.info("Confirmed watched paths:", watcher.getWatched());
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.started);
}
}
async function StopWatcher(): Promise<boolean> {
const mainWindow = getMainWindow();
if (watcher) {
await watcher.close();
log.info("Watcher stopped.");
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.stopped);
new Notification({
title: "Watcher Stopped",
body: "Estimates will not be automatically uploaded.",
}).show();
return true;
}
return false;
}
async function HandleNewFile(path): Promise<void> {
log.log("Received a new file", path);
await ImportJob(path);
}
function GetAllEnvFiles(): string[] {
const directories = store.get("settings.filepaths") as string[];
const files: string[] = [];
directories.forEach((directory) => {
try {
const envFiles = fs
.readdirSync(directory)
.filter((file: string) => file.toLowerCase().endsWith(".env"));
envFiles.forEach((file) => {
const fullPath = path.join(directory, file);
files.push(fullPath);
});
} catch (error) {
log.error(`Failed to read directory ${directory}:`, error);
throw error;
}
});
return files;
}
export {
addWatcherPath,
GetAllEnvFiles,
removeWatcherPath,
StartWatcher,
StopWatcher,
watcher,
};

8
src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import { ElectronAPI } from "@electron-toolkit/preload";
declare global {
interface Window {
electron: ElectronAPI;
api: unknown;
}
}

30
src/preload/index.ts Normal file
View File

@@ -0,0 +1,30 @@
import { contextBridge } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
import "electron-log/preload";
import store from "../main/store/store";
// Custom APIs for renderer
interface Api {
isTest: () => boolean;
}
const api: Api = {
isTest: (): boolean => store.get("app.isTest") || false,
};
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld("electron", electronAPI);
contextBridge.exposeInMainWorld("api", api);
} catch (error) {
console.error(error);
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI;
// @ts-ignore (define in dts)
window.api = api;
}

17
src/renderer/index.html Normal file
View File

@@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Shop Partner</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<!-- <meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/> -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,97 @@
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();
});
});

88
src/renderer/src/App.tsx Normal file
View File

@@ -0,0 +1,88 @@
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 { 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 (
<ConfigProvider
theme={{
token: {},
components: {
Card: {
borderRadius: 8,
colorBgBase: "#ffaacc",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
},
},
}}
>
<Provider store={reduxStore}>
<HashRouter>
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<NotificationProvider>
<Skeleton loading={user === 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>
)}
</Layout>
</Skeleton>
</NotificationProvider>
</ErrorBoundary>
</HashRouter>
</Provider>
</ConfigProvider>
);
};
export default App;

View File

@@ -0,0 +1,25 @@
import { FC } from "react";
import { Button, Result } from "antd";
import { FallbackProps } from "react-error-boundary";
import { useTranslation } from "react-i18next";
const ErrorBoundaryFallback: FC<FallbackProps> = ({
error,
resetErrorBoundary,
}) => {
const { t } = useTranslation();
return (
<Result
status={"500"}
title={t("errors.errorboundary")}
subTitle={error?.message}
extra={[
<Button key="try-again" onClick={resetErrorBoundary}>
Try again
</Button>,
]}
/>
);
};
export default ErrorBoundaryFallback;

View File

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

View File

@@ -0,0 +1,164 @@
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

@@ -0,0 +1,46 @@
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

@@ -0,0 +1,183 @@
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

@@ -0,0 +1,173 @@
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

@@ -0,0 +1,46 @@
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

@@ -0,0 +1,64 @@
import { DeleteFilled, FileAddFilled } from "@ant-design/icons";
import { Button, Card, Space, Timeline } from "antd";
import { useEffect, useState, FC } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const SettingsWatchedPaths: FC = () => {
const [watchedPaths, setWatchedPaths] = useState<string[]>([]);
const { t } = useTranslation();
useEffect(() => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.filepaths.get)
.then((paths: string[]) => {
setWatchedPaths(paths);
});
}, []);
const handleAddPath = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.filepaths.add)
.then((paths: string[]) => {
setWatchedPaths(paths);
});
};
const handleRemovePath = (path: string): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.filepaths.remove, path)
.then((paths: string[]) => {
setWatchedPaths(paths);
});
};
return (
<Card
title={t("settings.labels.watchedpaths")}
extra={
<Button onClick={handleAddPath} icon={<FileAddFilled />}>
{t("settings.actions.addpath")}
</Button>
}
>
<Timeline
items={watchedPaths.map((path, index) => ({
key: index,
children: (
<Space align="baseline">
{path}
<Button
size="small"
danger
type="text"
icon={<DeleteFilled />}
onClick={() => handleRemovePath(path)}
/>
</Space>
),
}))}
/>
</Card>
);
};
export default SettingsWatchedPaths;

View File

@@ -0,0 +1,157 @@
import { useAppSelector } from "@renderer/redux/reduxHooks";
import {
CheckCircleFilled,
ExclamationCircleFilled,
PauseCircleOutlined,
PlayCircleOutlined,
} from "@ant-design/icons";
import {
selectWatcherError,
selectWatcherStatus,
} from "@renderer/redux/app.slice";
import {
Alert,
Badge,
Button,
Card,
Col,
InputNumber,
Row,
Space,
Switch,
} from "antd";
import { FC, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const colSpans = {
md: 12,
sm: 24,
};
const SettingsWatcher: FC = () => {
const { t } = useTranslation();
const isWatcherStarted = useAppSelector(selectWatcherStatus);
const watcherError = useAppSelector(selectWatcherError);
const [pollingState, setPollingState] = useState<{
enabled: boolean;
interval: number;
}>({
enabled: false,
interval: 0,
});
const getPollingStateFromStore = (): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.watcher.getpolling)
.then((storePollingState: { enabled: boolean; interval: number }) => {
setPollingState(storePollingState);
});
};
//Get state first time it renders.
useEffect(() => {
getPollingStateFromStore();
}, []);
const handleStart = (): void => {
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start);
};
const handleStop = (): void => {
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.stop);
};
const toggleWatcherMode = (checked: boolean): void => {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.watcher.setpolling, {
enabled: !checked,
interval: pollingState.interval,
})
.then((storePollingState: { enabled: boolean; interval: number }) => {
setPollingState(storePollingState);
});
};
const handlePollingIntervalChange = (value: number | null): void => {
if (value) {
window.electron.ipcRenderer
.invoke(ipcTypes.toMain.settings.watcher.setpolling, {
enabled: pollingState.enabled,
interval: value,
})
.then((storePollingState: { enabled: boolean; interval: number }) => {
setPollingState(storePollingState);
});
}
getPollingStateFromStore();
};
return (
<Badge.Ribbon
text={
isWatcherStarted ? (
<Space>
<CheckCircleFilled />
{t("settings.labels.started")}
</Space>
) : (
<Space>
<ExclamationCircleFilled />
{t("settings.labels.stopped")}
</Space>
)
}
color={isWatcherStarted ? "green" : "red"}
>
<Card title={t("settings.labels.watcherstatus")}>
<Row gutter={[16, 16]}>
<Col {...colSpans}>
{isWatcherStarted ? (
<Button
danger
icon={<PauseCircleOutlined />}
onClick={handleStop}
>
{t("settings.actions.stopwatcher")}
</Button>
) : (
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={handleStart}
>
{t("settings.actions.startwatcher")}
</Button>
)}
</Col>
<Col {...colSpans}>
<Space direction="vertical" wrap>
<Switch
checked={!pollingState.enabled}
onChange={toggleWatcherMode}
checkedChildren={t("settings.labels.watchermoderealtime")}
unCheckedChildren={t("settings.labels.watchermodepolling")}
/>
{pollingState.enabled && (
<Space size="small" direction="vertical" wrap>
<span>{t("settings.labels.pollinginterval")}</span>
<InputNumber
title={t("settings.labels.pollinginterval")}
disabled={!pollingState.enabled}
min={1000}
value={pollingState.interval}
onChange={handlePollingIntervalChange}
/>
</Space>
)}
{watcherError && <Alert message={watcherError} />}
</Space>
</Col>
</Row>
</Card>
</Badge.Ribbon>
);
};
export default SettingsWatcher;

Some files were not shown because too many files have changed in this diff Show More