From b60d9b0f414a7e522f0bf6ba98a193ce84fcf600 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 2 Apr 2025 17:10:16 -0700 Subject: [PATCH] WIP CCC Part Price Change. --- src/main/http-server/http-server.ts | 2 + src/main/ipc/ipcMainConfig.ts | 6 + src/main/ipc/ipcMainHandler.settings.ts | 27 +- src/main/ppc/ppc-generate-file.ts | 12 + src/main/ppc/ppc-generate-lin.ts | 299 ++++++++++++++++++ src/main/ppc/ppc-handler.ts | 71 +++++ src/main/store/store.ts | 1 + .../NavigationHeader/Navigationheader.tsx | 5 +- .../Settings/Settings.PpcFilePath.tsx | 46 +++ .../src/components/Settings/Settings.tsx | 4 + src/util/ipcTypes.json | 2 + src/util/translations/en-US/renderer.json | 87 ++--- translations.babel | 13 + 13 files changed, 526 insertions(+), 49 deletions(-) create mode 100644 src/main/ppc/ppc-generate-file.ts create mode 100644 src/main/ppc/ppc-generate-lin.ts create mode 100644 src/main/ppc/ppc-handler.ts create mode 100644 src/renderer/src/components/Settings/Settings.PpcFilePath.tsx diff --git a/src/main/http-server/http-server.ts b/src/main/http-server/http-server.ts index 64bfcc9..bd7f358 100644 --- a/src/main/http-server/http-server.ts +++ b/src/main/http-server/http-server.ts @@ -6,6 +6,7 @@ import http from "http"; import errorTypeCheck from "../../util/errorTypeCheck"; import ImportJob from "../decoder/decoder"; import folderScan from "../decoder/folder-scan"; +import { handlePartsPariceChangeRequest } from "../ppc/ppc-handler"; import { handleQuickBookRequest } from "../quickbooks-desktop/quickbooks-desktop"; export default class LocalServer { @@ -117,6 +118,7 @@ export default class LocalServer { res.status(200).json(files); return; }); + this.app.post("/ppc", handlePartsPariceChangeRequest); this.app.post( "/import", async (req: express.Request, res: express.Response) => { diff --git a/src/main/ipc/ipcMainConfig.ts b/src/main/ipc/ipcMainConfig.ts index 78efde1..482fa6a 100644 --- a/src/main/ipc/ipcMainConfig.ts +++ b/src/main/ipc/ipcMainConfig.ts @@ -7,6 +7,8 @@ import ImportJob from "../decoder/decoder"; import store from "../store/store"; import { StartWatcher, StopWatcher } from "../watcher/watcher"; import { + SettingsPpcFilPathGet, + SettingsPpcFilPathSet, SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsGet, SettingsWatchedFilePathsRemove, @@ -93,6 +95,10 @@ ipcMain.handle( ipcTypes.toMain.settings.watcher.setpolling, SettingsWatcherPollingSet, ); + +ipcMain.handle(ipcTypes.toMain.settings.getPpcFilePath, SettingsPpcFilPathGet); +ipcMain.handle(ipcTypes.toMain.settings.setPpcFilePath, SettingsPpcFilPathSet); + ipcMain.handle(ipcTypes.toMain.user.getActiveShop, () => { return store.get("app.bodyshop.shopname"); }); diff --git a/src/main/ipc/ipcMainHandler.settings.ts b/src/main/ipc/ipcMainHandler.settings.ts index 66bf03e..6e6c120 100644 --- a/src/main/ipc/ipcMainHandler.settings.ts +++ b/src/main/ipc/ipcMainHandler.settings.ts @@ -1,7 +1,8 @@ -import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron"; +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, @@ -10,7 +11,7 @@ import { } from "../watcher/watcher"; const SettingsWatchedFilePathsAdd = async (): Promise => { - const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set. + const mainWindow = getMainWindow(); if (!mainWindow) { log.error("No main window found when trying to open dialog"); return []; @@ -74,8 +75,30 @@ const SettingsWatcherPollingSet = async ( return { enabled, interval }; }; +const SettingsPpcFilPathGet = async (): Promise => { + const ppcFilePath: string = Store.get("settings.ppcFilePath"); + return ppcFilePath; +}; +const SettingsPpcFilPathSet = async (): Promise => { + 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]); //There should only ever be on directory that was selected. + } + + return (Store.get("settings.ppcFilePath") as string) || ""; +}; export { + SettingsPpcFilPathGet, + SettingsPpcFilPathSet, SettingsWatchedFilePathsAdd, SettingsWatchedFilePathsGet, SettingsWatchedFilePathsRemove, diff --git a/src/main/ppc/ppc-generate-file.ts b/src/main/ppc/ppc-generate-file.ts new file mode 100644 index 0000000..4e652ff --- /dev/null +++ b/src/main/ppc/ppc-generate-file.ts @@ -0,0 +1,12 @@ +import path from "path"; +import store from "../store/store"; + +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); +}; + +export { generatePpcFilePath }; diff --git a/src/main/ppc/ppc-generate-lin.ts b/src/main/ppc/ppc-generate-lin.ts new file mode 100644 index 0000000..e10226a --- /dev/null +++ b/src/main/ppc/ppc-generate-lin.ts @@ -0,0 +1,299 @@ +import { DBFFile, FieldDescriptor } from "dbffile"; +import { generatePpcFilePath } from "./ppc-generate-file"; +import { PpcJob } from "./ppc-handler"; + +const GenerateLinFile = async (job: PpcJob): Promise => { + 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, + }; + }); + + let dbf = await DBFFile.create( + generatePpcFilePath(`${job.ciecaid}.LIN`), + linFieldDescriptors, + ); + console.log("DBF file created."); + await dbf.appendRecords(records); + console.log(`${records.length} LIN file records added.`); + return true; +}; + +export default GenerateLinFile; + +//Taken from a set of CCC ems files. +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, + }, +]; diff --git a/src/main/ppc/ppc-handler.ts b/src/main/ppc/ppc-handler.ts new file mode 100644 index 0000000..b31d4a3 --- /dev/null +++ b/src/main/ppc/ppc-handler.ts @@ -0,0 +1,71 @@ +import { UUID } from "crypto"; +import log from "electron-log/main"; +import express from "express"; +import fs from "fs"; +import _ from "lodash"; +import path from "path"; +import errorTypeCheck from "../../util/errorTypeCheck"; +import store from "../store/store"; +import GenerateLinFile from "./ppc-generate-lin"; + +const handlePartsPariceChangeRequest = async ( + req: express.Request, + res: express.Response, +): Promise => { + //Route handler here only. + + const { job } = req.body as { job: PpcJob }; + try { + await generatePartsPriceChange(job); + } 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; + } + res.status(200).json({ success: true }); + + return; +}; + +const generatePartsPriceChange = async (job: PpcJob): Promise => { + 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"); + } + + //If the directory doesn't exist, create it. + const directoryPath = path.dirname(ppcOutFilePath); + if (!fs.existsSync(directoryPath)) { + log.info(`Directory does not exist. Creating: ${directoryPath}`); + fs.mkdirSync(directoryPath, { recursive: true }); + } + const LinFile = await GenerateLinFile(job); + + //Generate the required file envelop to be sent back to CCC. +}; + +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 { handlePartsPariceChangeRequest }; diff --git a/src/main/store/store.ts b/src/main/store/store.ts index fa5d343..7874192 100644 --- a/src/main/store/store.ts +++ b/src/main/store/store.ts @@ -5,6 +5,7 @@ const store = new Store({ settings: { runOnStartup: true, filepaths: [], + ppcFilePath: null, qbFilePath: "", runWatcherOnStartup: true, polling: { diff --git a/src/renderer/src/components/NavigationHeader/Navigationheader.tsx b/src/renderer/src/components/NavigationHeader/Navigationheader.tsx index 31ecc8c..f581bc6 100644 --- a/src/renderer/src/components/NavigationHeader/Navigationheader.tsx +++ b/src/renderer/src/components/NavigationHeader/Navigationheader.tsx @@ -21,10 +21,7 @@ const NavigationHeader: React.FC = () => { ]; const isTest = window.api.isTest(); return ( - + { + const { t } = useTranslation(); + + const [ppcFilePath, setPpcFilePath] = useState(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 ( + + + +