Add watcher status and additional typing.

This commit is contained in:
Patrick Fic
2025-03-21 11:28:30 -07:00
parent 6da5822197
commit 14e7c64eab
19 changed files with 385 additions and 81 deletions

17
.prettierrc.js Normal file
View File

@@ -0,0 +1,17 @@
export default {
printWidth: 120,
useTabs: false,
tabWidth: 2,
trailingComma: "none",
semi: true,
singleQuote: false,
bracketSpacing: true,
arrowParens: "always",
jsxSingleQuote: false,
bracketSameLine: false,
endOfLine: "lf"
// importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
// importOrderSeparation: true,
// importOrderSortSpecifiers: true
};

View File

@@ -1,5 +1,5 @@
import tseslint from "@electron-toolkit/eslint-config-ts";
import eslintConfigPrettier from "@electron-toolkit/eslint-config-prettier";
import tseslint from "@electron-toolkit/eslint-config-ts";
import eslintPluginReact from "eslint-plugin-react";
import eslintPluginReactHooks from "eslint-plugin-react-hooks";
import eslintPluginReactRefresh from "eslint-plugin-react-refresh";
@@ -27,5 +27,5 @@ export default tseslint.config(
...eslintPluginReactRefresh.configs.vite.rules,
},
},
eslintConfigPrettier
eslintConfigPrettier,
);

View File

@@ -6,7 +6,7 @@ import errorTypeCheck from "../../util/errorTypeCheck";
import { DecodedStl, DecodedStlLine } from "./decode-stl.interface";
const DecodeStl = async (
extensionlessFilePath: string
extensionlessFilePath: string,
): Promise<DecodedStl> => {
let dbf: DBFFile | null = null;
try {
@@ -47,7 +47,7 @@ const DecodeStl = async (
"TTL_TYPAMT",
"TTL_HRS",
"TTL_AMT",
])
]),
);
//Apply line by line adjustments.

View File

@@ -15,7 +15,7 @@ export interface DecodedVeh {
impact2?: string;
};
// Complete vehicle data object
vehicle: { data: VehicleRecordInterface };
vehicle?: { data: VehicleRecordInterface };
}
export interface VehicleRecordInterface {

View File

@@ -1,10 +1,22 @@
import { UUID } from "crypto";
import log from "electron-log/main";
import fs from "fs";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
import client from "../graphql/graphql-client";
import {
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";
@@ -25,17 +37,12 @@ import DecodeTtl from "./decode-ttl";
import { DecodedTtl } from "./decode-ttl.interface";
import DecodeVeh from "./decode-veh";
import { DecodedVeh } from "./decode-veh.interface";
import { DecodedEnv } from "./decode-env.interface";
import DecodeEnv from "./decode-env";
import fs from "fs";
import store from "../store/store";
import client from "../graphql/graphql-client";
async function ImportJob(filepath: string): Promise<void> {
const parsedFilePath = path.parse(filepath);
const extensionlessFilePath = path.join(
parsedFilePath.dir,
parsedFilePath.name
parsedFilePath.name,
);
log.debug("Importing Job", extensionlessFilePath);
@@ -56,7 +63,7 @@ async function ImportJob(filepath: string): Promise<void> {
const ttl: DecodedTtl = await DecodeTtl(extensionlessFilePath);
const pfp: DecodedPfp = await DecodePfp(extensionlessFilePath);
const jobObject = {
const jobObject: RawJobDataObject = {
...env,
...ad1,
...ad2,
@@ -96,17 +103,7 @@ async function ImportJob(filepath: string): Promise<void> {
//Build the request object
//Insert it
const newAvailableJob = {
// newJob.uploaded_by = Auth.authlink.User.Email;
// newJob.bodyshopid = AppMetaData.ActiveShopId;
// newJob.cieca_id = item.Job.ciecaid;
// newJob.est_data = item.Job;
// newJob.ownr_name = item.Job.ownr_fn?.Value + " " + item.Job.ownr_ln?.Value + " " + item.Job.ownr_co_nm?.Value;
// newJob.ins_co_nm = item.Job.ins_co_nm?.Value;
// newJob.vehicle_info = item.Job.vehicle.data.v_model_yr?.Value + " " + item.Job.vehicle.data.v_make_desc?.Value + " " + item.Job.vehicle.data.v_model_desc?.Value;
// newJob.clm_no = item.Job.clm_no?.Value;
// newJob.clm_amt = item.Job.clm_total?.Value;
// newJob.source_system = item.Job.source_system?.Value;
const newAvailableJob: AvailableJobSchema = {
uploaded_by: store.get("app.user.email"),
bodyshopid: store.get("app.bodyshop.id"),
cieca_id: jobObject.ciecaid,
@@ -117,30 +114,68 @@ async function ImportJob(filepath: string): Promise<void> {
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,
};
const existingVehicleId: uuid = await client.query ()
// var vehuuid = await Utils.Queries.VehicleQueries.GetVehicleUuidByVin(item?.Job?.vehicle?.data?.v_vin?.Value ?? "");
// if (!string.IsNullOrEmpty(vehuuid))
// {
// newJob.est_data.vehicle = null;
// newJob.est_data.vehicleid = vehuuid;
// }
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;
}
// string jobId = await Utils.Queries.JobsQueries.CheckSupplementByClaimNo(item.Job.clm_no?.Value ?? "");
console.log(newAvailableJob);
// if (!string.IsNullOrEmpty(jobId))
// {
// newJob.issupplement = true;
// newJob.jobid = jobId;
// };
//Check if the vehicle exists, if it does, use that UUID, if not, keep it to insert it.
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;
}
} catch (error) {
log.error("Error encountered while decoding job. ", errorTypeCheck(error));
}
}
export default ImportJob;
export interface RawJobDataObject
extends DecodedEnv,
DecodedAd1,
DecodedAD2,
DecodedVeh,
DecodedLin,
DecodedPfh,
DecodedPfl,
DecodedPft,
DecodedPfm,
DecodedPfo,
DecodedStl,
DecodedTtl,
DecodedPfp {
vehicleid?: 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;
}

View File

@@ -1,6 +1,7 @@
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<{
@@ -9,10 +10,8 @@ export interface ActiveBodyshopQueryResult {
region_config: string;
}>;
}
// No variables needed for this query
interface ActiveBodyshopQueryVariables {}
// Transform the string query into a TypedQueryDocumentNode
export const QUERY_ACTIVE_BODYSHOP_TYPED: TypedQueryDocumentNode<
ActiveBodyshopQueryResult,
@@ -25,7 +24,10 @@ export const QUERY_ACTIVE_BODYSHOP_TYPED: TypedQueryDocumentNode<
region_config
}
}
`);
`) as TypedQueryDocumentNode<
ActiveBodyshopQueryResult,
ActiveBodyshopQueryVariables
>;
export interface MasterdataQueryResult {
masterdata: Array<{
@@ -33,11 +35,9 @@ export interface MasterdataQueryResult {
key: string;
}>;
}
interface MasterdataQueryVariables {
key: string;
}
export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode<
MasterdataQueryResult,
MasterdataQueryVariables
@@ -48,19 +48,16 @@ export const QUERY_MASTERDATA_TYPED: TypedQueryDocumentNode<
key
}
}
`);
`) as TypedQueryDocumentNode<MasterdataQueryResult, MasterdataQueryVariables>;
export interface VehicleQueryResult {
masterdata: Array<{
value: string;
key: string;
vehicles: Array<{
id: UUID;
}>;
}
interface VehicleQueryVariables {
vin: string;
}
export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode<
VehicleQueryResult,
VehicleQueryVariables
@@ -70,4 +67,62 @@ export const QUERY_VEHICLE_BY_VIN_TYPED: TypedQueryDocumentNode<
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
>;

View File

@@ -3,7 +3,7 @@ import log from "electron-log/main";
import path from "path";
import ipcTypes from "../../util/ipcTypes.json";
import ImportJob from "../decoder/decoder";
import { StartWatcher } from "../watcher/watcher";
import { StartWatcher, StopWatcher } from "../watcher/watcher";
import {
SettingsWatchedFilePathsAdd,
SettingsWatchedFilePathsGet,
@@ -83,4 +83,8 @@ ipcMain.on(ipcTypes.toMain.watcher.start, () => {
StartWatcher();
});
ipcMain.on(ipcTypes.toMain.watcher.stop, () => {
StopWatcher();
});
logIpcMessages();

View File

@@ -2,6 +2,7 @@ import { BrowserWindow, dialog, IpcMainInvokeEvent } from "electron";
import log from "electron-log/main";
import _ from "lodash";
import Store from "../store/store";
import { addWatcherPath, removeWatcherPath, watcher } 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.
@@ -18,6 +19,7 @@ const SettingsWatchedFilePathsAdd = async (): Promise<string[]> => {
"settings.filepaths",
_.union(result.filePaths, Store.get("settings.filepaths"))
);
addWatcherPath(result.filePaths);
}
return Store.get("settings.filepaths");
@@ -30,7 +32,7 @@ const SettingsWatchedFilePathsRemove = async (
"settings.filepaths",
_.without(Store.get("settings.filepaths"), path)
);
removeWatcherPath(path);
return Store.get("settings.filepaths");
};

View File

@@ -1,10 +1,11 @@
import chokidar, { FSWatcher } from "chokidar";
import { Notification } from "electron";
import { BrowserWindow, Notification } from "electron";
import log from "electron-log/main";
import path from "path";
import errorTypeCheck from "../../util/errorTypeCheck";
import store from "../store/store";
import ipcTypes from "../../util/ipcTypes.json";
import ImportJob from "../decoder/decoder";
import store from "../store/store";
let watcher: FSWatcher;
@@ -67,6 +68,10 @@ async function StartWatcher(): Promise<boolean> {
// })
.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) {
@@ -77,24 +82,37 @@ async function StartWatcher(): Promise<boolean> {
return true;
}
function removeWatcherPath(path: string): void {
watcher.unwatch(path);
log.debug(`Stopped watching path: ${path}`);
}
function addWatcherPath(path: string | string[]): void {
watcher.add(path);
log.debug(`Started watching path: ${path}`);
}
function onWatcherReady(): void {
log.info("Watcher ready!");
// const b = BrowserWindow.getAllWindows()[0];
// b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.startSuccess);
const mainWindow = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set.
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 = BrowserWindow.getAllWindows()[0]; //TODO: Filter to only main window once a proper key has been set.
if (watcher) {
await watcher.close();
log.info("Watcher stopped.");
mainWindow.webContents.send(ipcTypes.toRenderer.watcher.stopped);
new Notification({
title: "RPS Watcher Stopped",
title: "Watcher Stopped",
body: "Estimates will not be automatically uploaded.",
}).show();
return true;
@@ -107,4 +125,10 @@ async function HandleNewFile(path): Promise<void> {
log.log("Received a new file", path);
}
export { StartWatcher, StopWatcher, watcher };
export {
StartWatcher,
StopWatcher,
watcher,
removeWatcherPath,
addWatcherPath,
};

View File

@@ -1,16 +1,50 @@
import { Button } from "antd";
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons";
import {
selectWatcherError,
selectWatcherStatus,
} from "@renderer/redux/app.slice";
import { useAppDispatch, useAppSelector } from "@renderer/redux/reduxHooks";
import { Button, Space } from "antd";
import { useTranslation } from "react-i18next";
import ipcTypes from "../../../../util/ipcTypes.json";
const SettingsWatcher: React.FC = () => {
const { t } = useTranslation();
const isWatcherStarted = useAppSelector(selectWatcherStatus);
const watcherError = useAppSelector(selectWatcherError);
const dispatch = useAppDispatch();
const handleStart = (): void => {
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.start);
};
const handleStop = (): void => {
window.electron.ipcRenderer.send(ipcTypes.toMain.watcher.stop);
};
return (
<Button onClick={handleStart}>{t("settings.actions.startwatcher")}</Button>
<>
<Button onClick={handleStart}>
{t("settings.actions.startwatcher")}
</Button>
<Button onClick={handleStop}>{t("settings.actions.stopwatcher")}</Button>
{isWatcherStarted}
{watcherError}
{isWatcherStarted ? (
<Space>
<CheckCircleOutlined style={{ color: "green" }} />
{t("settings.labels.started")}
</Space>
) : (
<Space>
<ExclamationCircleOutlined style={{ color: "tomato" }} />
{t("settings.labels.stopped")}
</Space>
)}
</>
);
};
export default SettingsWatcher;

View File

@@ -1,46 +1,58 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import log from "electron-log/renderer";
import type { RootState } from "./redux-store";
// Define a type for the slice state
interface AppState {
value: number;
watcher: {
started: boolean;
error: string | null;
};
}
// Define the initial state using that type
const initialState: AppState = {
value: 0,
watcher: {
started: false,
error: null,
},
};
export const appSlice = createSlice({
name: "counter",
name: "app",
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: (state) => {
state.value += 1;
watcherStarted: (state) => {
state.watcher.started = true;
},
decrement: (state) => {
state.value -= 1;
watcherStopped: (state) => {
state.watcher.started = false;
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
watcherError: (state, action: PayloadAction<string>) => {
state.watcher.error = action.payload;
state.watcher.started = false;
log.error("[Redux] AppSlice: Watcher Error", action.payload);
},
},
});
export const { increment, decrement, incrementByAmount } = appSlice.actions;
export const { watcherError, watcherStarted, watcherStopped } =
appSlice.actions;
// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState): number => state.app.value;
export const selectWatcherStatus = (state: RootState): boolean =>
state.app.watcher.started;
export const selectWatcherError = (state: RootState): string | null =>
state.app.watcher.error;
//Async Functions - Thunks
// Define a thunk that dispatches those action creators
const fetchUsers = () => async (dispatch) => {
dispatch(increment());
//dispatch(watcherStarted());
//Some sort of async action.
dispatch(incrementByAmount(100));
// dispatch(incrementByAmount(100));
};
export default appSlice.reducer;

View File

@@ -1,9 +1,10 @@
import type { TypedUseSelectorHook } from "react-redux";
import { useDispatch, useSelector, useStore } from "react-redux";
import type { AppDispatch, AppStore, RootState } from "./redux-store";
import store from "./redux-store";
//Use these custom hooks to access the Redux store from your component with type safety.
export const useAppDispatch: () => AppDispatch = useDispatch;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); // Ex
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useAppStore: () => AppStore = useStore;

View File

@@ -1,8 +1,15 @@
//Set up all of the IPC handlers.
import {
watcherError,
watcherStarted,
watcherStopped,
} from "@renderer/redux/app.slice";
import store from "@renderer/redux/redux-store";
import ipcTypes from "../../../util/ipcTypes.json";
import { auth } from "./firebase";
const ipcRenderer = window.electron.ipcRenderer;
const dispatch = store.dispatch;
ipcRenderer.on(
ipcTypes.toRenderer.test,
@@ -19,3 +26,29 @@ ipcRenderer.on(
ipcRenderer.send(ipcTypes.toMain.user.getTokenResponse, token);
}
);
ipcRenderer.on(
ipcTypes.toRenderer.watcher.started,
(event: Electron.IpcRendererEvent, arg) => {
console.log("Watcher has started");
console.log(arg);
dispatch(watcherStarted());
}
);
ipcRenderer.on(
ipcTypes.toRenderer.watcher.stopped,
(event: Electron.IpcRendererEvent, arg) => {
console.log("Watcher has stopped");
console.log(arg);
dispatch(watcherStopped());
}
);
ipcRenderer.on(
ipcTypes.toRenderer.watcher.error,
(event: Electron.IpcRendererEvent, error: string) => {
console.log("Watcher has encountered an error");
console.log(error);
dispatch(watcherError(error));
}
);

View File

@@ -22,6 +22,11 @@
},
"toRenderer": {
"test": "toRenderer_test",
"watcher": {
"started": "toRenderer_watcher_started",
"stopped": "toRenderer_watcher_stopped",
"error": "toRenderer_watcher_error"
},
"user": {
"getToken": "toRenderer_user_getToken"
}

View File

@@ -6,7 +6,14 @@
},
"settings": {
"actions": {
"addpath": "Add path"
"addpath": "Add path",
"startwatcher": "Start Watcher",
"stopwatcher": "Stop Watcher\n"
},
"labels": {
"started": "Started",
"stopped": "Stopped",
"watcherstatus": "Watcher Status"
}
}
}

View File

@@ -95,6 +95,76 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>startwatcher</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>stopwatcher</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>
</children>
</folder_node>
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>started</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>stopped</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>watcherstatus</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>
</children>
</folder_node>
</children>

View File

@@ -1,4 +1,7 @@
{
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.web.json" }
]
}

View File

@@ -9,6 +9,7 @@
"tests/index.spec.ts"
],
"compilerOptions": {
"resolveJsonModule": true,
"composite": true,
"types": ["electron-vite/node"]
}

View File

@@ -8,6 +8,7 @@
"src/util/**/*"
],
"compilerOptions": {
"resolveJsonModule": true,
"composite": true,
"jsx": "react-jsx",
"baseUrl": ".",