WIP CCC Part Price Change.

This commit is contained in:
Patrick Fic
2025-04-02 17:10:16 -07:00
parent da18d3308f
commit b60d9b0f41
13 changed files with 526 additions and 49 deletions

View File

@@ -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) => {

View File

@@ -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");
});

View File

@@ -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<string[]> => {
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<string> => {
const ppcFilePath: string = Store.get("settings.ppcFilePath");
return ppcFilePath;
};
const SettingsPpcFilPathSet = 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]); //There should only ever be on directory that was selected.
}
return (Store.get("settings.ppcFilePath") as string) || "";
};
export {
SettingsPpcFilPathGet,
SettingsPpcFilPathSet,
SettingsWatchedFilePathsAdd,
SettingsWatchedFilePathsGet,
SettingsWatchedFilePathsRemove,

View File

@@ -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 };

View File

@@ -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<boolean> => {
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,
},
];

View File

@@ -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<void> => {
//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<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");
}
//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 };

View File

@@ -5,6 +5,7 @@ const store = new Store({
settings: {
runOnStartup: true,
filepaths: [],
ppcFilePath: null,
qbFilePath: "",
runWatcherOnStartup: true,
polling: {

View File

@@ -21,10 +21,7 @@ const NavigationHeader: React.FC = () => {
];
const isTest = window.api.isTest();
return (
<Badge.Ribbon
text={isTest && "Connected to Test Environment"}
color={isTest && "red"}
>
<Badge.Ribbon text={isTest && "TEST ENV"} color={isTest && "red"}>
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
<Menu
theme="dark"

View File

@@ -0,0 +1,46 @@
import { FolderOpenFilled } from "@ant-design/icons";
import { Button, Card, Input, Space } from "antd";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const SettingsPpcFilepath: React.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

@@ -2,6 +2,7 @@ import { Col, Row } from "antd";
import SettingsWatchedPaths from "./Settings.WatchedPaths";
import SettingsWatcher from "./Settings.Watcher";
import Welcome from "../Welcome/Welcome";
import SettingsPpcFilepath from "./Settings.PpcFilePath";
const Settings: React.FC = () => {
console.log("is test?", window.api.isTest());
@@ -16,6 +17,9 @@ const Settings: React.FC = () => {
<Col span={12}>
<SettingsWatcher />
</Col>
<Col span={12}>
<SettingsPpcFilepath />
</Col>
</Row>
);
};

View File

@@ -20,6 +20,8 @@
"add": "toMain_settings_filepaths_add",
"remove": "toMain_settings_filepaths_remove"
},
"getPpcFilePath": "toMain_settings_filepaths_getPpcFilePath",
"setPpcFilePath": "toMain_settings_filepaths_setPpcFilePath",
"watcher": {
"getpolling": "toMain_settings_watcher_getpolling",
"setpolling": "toMain_settings_watcher_setpolling"

View File

@@ -1,45 +1,46 @@
{
"translation": {
"auth": {
"labels": {
"welcome": "Hi {{name}}"
},
"login": {
"error": "The username and password combination provided is not valid.",
"login": "Log In",
"resetpassword": "Reset Password"
}
},
"errors": {
"errorboundary": "Uh oh - we've hit an error.",
"notificationtitle": "Error Encountered"
},
"navigation": {
"home": "Home",
"settings": "Settings",
"signout": "Sign Out"
},
"settings": {
"actions": {
"addpath": "Add path",
"startwatcher": "Start Watcher",
"stopwatcher": "Stop Watcher\n"
},
"labels": {
"pollinginterval": "Polling Interval (ms)",
"started": "Started",
"stopped": "Stopped",
"watchedpaths": "Watched Paths",
"watchermodepolling": "Polling",
"watchermoderealtime": "Real Time",
"watcherstatus": "Watcher Status"
}
},
"updates": {
"apply": "Apply Update",
"available": "An update is available.",
"download": "Download Update",
"downloading": "An update is downloading."
}
}
"translation": {
"auth": {
"labels": {
"welcome": "Hi {{name}}"
},
"login": {
"error": "The username and password combination provided is not valid.",
"login": "Log In",
"resetpassword": "Reset Password"
}
},
"errors": {
"errorboundary": "Uh oh - we've hit an error.",
"notificationtitle": "Error Encountered"
},
"navigation": {
"home": "Home",
"settings": "Settings",
"signout": "Sign Out"
},
"settings": {
"actions": {
"addpath": "Add path",
"startwatcher": "Start Watcher",
"stopwatcher": "Stop Watcher\n"
},
"labels": {
"pollinginterval": "Polling Interval (ms)",
"ppcfilepath": "Parts Price Change File Path",
"started": "Started",
"stopped": "Stopped",
"watchedpaths": "Watched Paths",
"watchermodepolling": "Polling",
"watchermoderealtime": "Real Time",
"watcherstatus": "Watcher Status"
}
},
"updates": {
"apply": "Apply Update",
"available": "An update is available.",
"download": "Download Update",
"downloading": "An update is downloading."
}
}
}

View File

@@ -250,6 +250,19 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ppcfilepath</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>started</name>
<definition_loaded>false</definition_loaded>