feature/IO-3702-ESPD-UI-AND-FIXES - Stage 2
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { app } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import Database from "better-sqlite3";
|
||||
import crypto from "crypto";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import path from "path";
|
||||
import type { RawJobDataObject } from "../decoder/decoder";
|
||||
|
||||
@@ -64,7 +64,7 @@ type IdentifiedItem = {
|
||||
LinkText?: unknown;
|
||||
};
|
||||
|
||||
let db: Database.Database | undefined;
|
||||
let db: DatabaseSync | undefined;
|
||||
|
||||
function toNullableString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
@@ -84,15 +84,31 @@ function getDbPath(): string {
|
||||
return path.join(userDataDir, "scrub-history.sqlite3");
|
||||
}
|
||||
|
||||
function getDb(): Database.Database {
|
||||
function runTransaction<T>(database: DatabaseSync, callback: () => T): T {
|
||||
database.exec("BEGIN");
|
||||
try {
|
||||
const result = callback();
|
||||
database.exec("COMMIT");
|
||||
return result;
|
||||
} catch (error) {
|
||||
try {
|
||||
database.exec("ROLLBACK");
|
||||
} catch (rollbackError) {
|
||||
log.error("[scrub-history-db] rollback failed", rollbackError);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getDb(): DatabaseSync {
|
||||
if (db) return db;
|
||||
|
||||
const dbPath = getDbPath();
|
||||
log.info(`[scrub-history-db] opening sqlite db at ${dbPath}`);
|
||||
db = new Database(dbPath);
|
||||
db = new DatabaseSync(dbPath);
|
||||
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("foreign_keys = ON");
|
||||
db.exec("PRAGMA journal_mode = WAL");
|
||||
db.exec("PRAGMA foreign_keys = ON");
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
@@ -182,7 +198,7 @@ export function insertScrubRun(params: {
|
||||
? [params.identifiedItems as IdentifiedItem]
|
||||
: [];
|
||||
|
||||
const insertTx = database.transaction(() => {
|
||||
runTransaction(database, () => {
|
||||
database
|
||||
.prepare(
|
||||
"INSERT INTO jobs (id, created_at, ownr_name, vehicle, claim_number, pdf_url, report_issue_url) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
@@ -194,7 +210,7 @@ export function insertScrubRun(params: {
|
||||
jobRow.vehicle,
|
||||
jobRow.claim_number,
|
||||
jobRow.pdf_url,
|
||||
jobRow.report_issue_url,
|
||||
jobRow.report_issue_url ?? null,
|
||||
);
|
||||
|
||||
const stmt = database.prepare(
|
||||
@@ -215,7 +231,6 @@ export function insertScrubRun(params: {
|
||||
}
|
||||
});
|
||||
|
||||
insertTx();
|
||||
return { jobId: jobRow.id, createdAt, insertedResultsCount: items.length };
|
||||
}
|
||||
|
||||
@@ -379,24 +394,26 @@ export function deleteScrubHistoryJob(jobId: string): { deletedJobs: number } {
|
||||
const trimmed = jobId.trim();
|
||||
if (!trimmed) return { deletedJobs: 0 };
|
||||
|
||||
const tx = database.transaction(() => {
|
||||
const res = database.prepare("DELETE FROM jobs WHERE id = ?").run(trimmed);
|
||||
return res.changes;
|
||||
});
|
||||
|
||||
return { deletedJobs: tx() };
|
||||
return {
|
||||
deletedJobs: runTransaction(database, () => {
|
||||
const res = database
|
||||
.prepare("DELETE FROM jobs WHERE id = ?")
|
||||
.run(trimmed);
|
||||
return Number(res.changes);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function clearScrubHistory(): { clearedJobs: number } {
|
||||
const database = getDb();
|
||||
|
||||
const tx = database.transaction(() => {
|
||||
const countBefore = database
|
||||
.prepare("SELECT COUNT(1) as c FROM jobs")
|
||||
.get() as { c: number };
|
||||
database.prepare("DELETE FROM jobs").run();
|
||||
return countBefore?.c ?? 0;
|
||||
});
|
||||
|
||||
return { clearedJobs: tx() };
|
||||
return {
|
||||
clearedJobs: runTransaction(database, () => {
|
||||
const countBefore = database
|
||||
.prepare("SELECT COUNT(1) as c FROM jobs")
|
||||
.get() as { c: number };
|
||||
database.prepare("DELETE FROM jobs").run();
|
||||
return countBefore?.c ?? 0;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ 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";
|
||||
@@ -51,7 +50,6 @@ const DecodeLin = async (
|
||||
|
||||
//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(
|
||||
|
||||
@@ -4,7 +4,7 @@ import _ from "lodash";
|
||||
import deepLowerCaseKeys from "../../util/deepLowercaseKeys";
|
||||
import errorTypeCheck from "../../util/errorTypeCheck";
|
||||
import { DecodedPfh } from "./decode-pfh.interface";
|
||||
import { platform } from "os";
|
||||
import { platform } from "@electron-toolkit/utils";
|
||||
import { findFileCaseInsensitive } from "./decoder-utils";
|
||||
|
||||
const DecodePfh = async (
|
||||
@@ -48,7 +48,7 @@ const DecodePfh = async (
|
||||
const rawDBFRecord = await dbf.readRecords(1);
|
||||
|
||||
//AD2 will always have only 1 row.
|
||||
//Commented lines have been cross referenced with existing partner fields.
|
||||
//Commented lines have been cross-referenced with existing partner fields.
|
||||
|
||||
const rawPfhData: DecodedPfh = deepLowerCaseKeys(
|
||||
_.pick(rawDBFRecord[0], [
|
||||
@@ -76,7 +76,7 @@ const DecodePfh = async (
|
||||
]),
|
||||
);
|
||||
|
||||
//Apply business logic transfomrations.
|
||||
//Apply business logic transformations.
|
||||
|
||||
//Standardize some of the numbers and divide by 100.
|
||||
|
||||
|
||||
@@ -37,11 +37,4 @@ const findFileCaseInsensitive = async (
|
||||
return null;
|
||||
};
|
||||
|
||||
const getFilePathWithoutExtension = (filePath: string): string => {
|
||||
return path.join(
|
||||
path.dirname(filePath),
|
||||
path.basename(filePath, path.extname(filePath)),
|
||||
);
|
||||
};
|
||||
|
||||
export { findFileCaseInsensitive };
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
@@ -38,6 +37,7 @@ import { DecodedVeh } from "./decode-veh.interface";
|
||||
import UploadEmsToS3 from "./emsbackup";
|
||||
import getMainWindow from "../../util/getMainWindow";
|
||||
import newWindow from "../../util/newWindow";
|
||||
import { createNotification, showNotification } from "../util/notification";
|
||||
|
||||
async function ImportJob(filepath: string): Promise<void> {
|
||||
const parsedFilePath = path.parse(filepath);
|
||||
@@ -151,7 +151,7 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
});
|
||||
setAppProgressbar(-1);
|
||||
|
||||
const uploadNotification = new Notification({
|
||||
const uploadNotification = createNotification({
|
||||
title: "Job Scrubbed",
|
||||
body: `${newAvailableJob.ownr_name} - ${newAvailableJob.vehicle_info}.`,
|
||||
actions: [
|
||||
@@ -175,12 +175,10 @@ async function ImportJob(filepath: string): Promise<void> {
|
||||
uploadNotification.show();
|
||||
} catch (error) {
|
||||
log.error("Error encountered while decoding job. ", errorTypeCheck(error));
|
||||
const uploadNotificationFailure = new Notification({
|
||||
showNotification({
|
||||
title: "Job Scrub Failure",
|
||||
body: errorTypeCheck(error).message, //TODO: Remove after debug.
|
||||
});
|
||||
|
||||
uploadNotificationFailure.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +201,7 @@ export interface RawJobDataObject
|
||||
DecodedPfp {
|
||||
vehicleid?: UUID;
|
||||
shopid: UUID;
|
||||
source_system?: string | null;
|
||||
}
|
||||
|
||||
export interface AvailableJobSchema {
|
||||
|
||||
@@ -5,8 +5,6 @@ import errorTypeCheck from "../../util/errorTypeCheck";
|
||||
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,
|
||||
|
||||
@@ -164,6 +164,7 @@ export interface ESJobObject extends Omit<
|
||||
| "v_model_desc"
|
||||
| "shopid"
|
||||
| "est_system"
|
||||
| "id_pro_nam"
|
||||
|
||||
// Object fields
|
||||
| "owner"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { BrowserWindow } from "electron";
|
||||
import log from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { promises as fsPromises } from "fs";
|
||||
@@ -8,8 +7,21 @@ import { RawJobDataObject } from "../decoder/decoder";
|
||||
import store from "../store/store";
|
||||
import ipcTypes from "../../util/ipcTypes.json";
|
||||
import { insertScrubRun } from "../db/scrub-history-db";
|
||||
import { Notification } from "electron/main";
|
||||
import getMainWindow from "../../util/getMainWindow";
|
||||
import { showNotification } from "../util/notification";
|
||||
|
||||
function getErrorMessage(responseMessage: string | undefined): string | null {
|
||||
if (!responseMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedResponse = JSON.parse(responseMessage) as { message?: string };
|
||||
return parsedResponse.message ?? null;
|
||||
} catch {
|
||||
return responseMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to write job object to logs subfolder
|
||||
async function writeJobToLogsFolder(job, fileName): Promise<string> {
|
||||
@@ -75,13 +87,11 @@ async function ScrubEstimate({
|
||||
const esApiKey = store.get("settings.esApiKey") as string;
|
||||
|
||||
if (!esApiKey || esApiKey.trim() === "") {
|
||||
const notificationError = new Notification({
|
||||
showNotification({
|
||||
title: "No API Key Set",
|
||||
body: "You must have a valid and active API key set in order to scrub estimates.",
|
||||
});
|
||||
|
||||
notificationError.show();
|
||||
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, {
|
||||
message:
|
||||
"You must have a valid and active API key set in order to scrub estimates.",
|
||||
@@ -127,7 +137,6 @@ async function ScrubEstimate({
|
||||
report_issue_url:
|
||||
typeof report_issue_url === "string" ? report_issue_url : null,
|
||||
});
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(ipcTypes.toRenderer.scrub.historyUpdated);
|
||||
}
|
||||
@@ -158,27 +167,24 @@ async function ScrubEstimate({
|
||||
: responseData
|
||||
? JSON.stringify(responseData)
|
||||
: undefined;
|
||||
const scrubErrorMessage = getErrorMessage(responseMessage);
|
||||
|
||||
if (status === 400) {
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, {
|
||||
message:
|
||||
JSON.parse(responseMessage).message ||
|
||||
scrubErrorMessage ||
|
||||
"Error encountered sending estimate to Estimate Scrubber.",
|
||||
});
|
||||
} else if (status === 401) {
|
||||
const notificationError = new Notification({
|
||||
showNotification({
|
||||
title: "Error scrubbing estimate",
|
||||
body:
|
||||
JSON.parse(responseMessage).message ||
|
||||
"Authentication with Estimate Scrubber failed.",
|
||||
scrubErrorMessage || "Authentication with Estimate Scrubber failed.",
|
||||
});
|
||||
|
||||
notificationError.show();
|
||||
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.scrub.scrubError, {
|
||||
message:
|
||||
JSON.parse(responseMessage).message ||
|
||||
"Authentication with Estimate Scrubber failed.",
|
||||
scrubErrorMessage || "Authentication with Estimate Scrubber failed.",
|
||||
});
|
||||
}
|
||||
return "Error: Unable to scrub estimate.";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserWindow, ipcMain } from "electron";
|
||||
import { ipcMain } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import { GraphQLClient, RequestMiddleware } from "graphql-request";
|
||||
import errorTypeCheck from "../../util/errorTypeCheck.js";
|
||||
|
||||
@@ -13,7 +13,7 @@ test("Basic Electron app compilation.", async () => {
|
||||
|
||||
// Wait for the first BrowserWindow to open
|
||||
// and return its Page object
|
||||
const window = await electronApp.firstWindow();
|
||||
await electronApp.firstWindow();
|
||||
// close app
|
||||
await electronApp.close();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { is, optimizer, platform } from "@electron-toolkit/utils";
|
||||
import Sentry from "@sentry/electron/main";
|
||||
import * as Sentry from "@sentry/electron/main";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
@@ -105,7 +105,7 @@ function createWindow(): void {
|
||||
},
|
||||
});
|
||||
|
||||
const template: Electron.MenuItemConstructorOptions[] = [
|
||||
const template = [
|
||||
// { role: 'appMenu' }
|
||||
// @ts-ignore
|
||||
...(isMac
|
||||
@@ -387,7 +387,7 @@ function createWindow(): void {
|
||||
: [{ role: "close" }]),
|
||||
],
|
||||
},
|
||||
];
|
||||
] as Electron.MenuItemConstructorOptions[];
|
||||
|
||||
const menu: Electron.Menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
|
||||
@@ -27,10 +27,15 @@ import newWindow from "../../util/newWindow";
|
||||
const logIpcMessages = (): void => {
|
||||
Object.keys(ipcTypes.toMain).forEach((key) => {
|
||||
const messageType = ipcTypes.toMain[key];
|
||||
const originalHandler = ipcMain.listeners(messageType)?.[0];
|
||||
if (originalHandler) {
|
||||
if (typeof messageType !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalHandlers = ipcMain.listeners(messageType);
|
||||
if (originalHandlers.length > 0) {
|
||||
ipcMain.removeAllListeners(messageType);
|
||||
}
|
||||
|
||||
ipcMain.on(messageType, (event, payload) => {
|
||||
log.info(
|
||||
`%c[IPC Main]%c${messageType}`,
|
||||
@@ -38,9 +43,7 @@ const logIpcMessages = (): void => {
|
||||
"color: green",
|
||||
payload,
|
||||
);
|
||||
if (originalHandler) {
|
||||
originalHandler(event, payload);
|
||||
}
|
||||
originalHandlers.forEach((handler) => handler(event, payload));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -102,7 +105,7 @@ ipcMain.on(ipcTypes.toMain.updates.download, () => {
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(ipcTypes.toMain.openExternal, async (event, url: string) => {
|
||||
ipcMain.on(ipcTypes.toMain.openExternal, async (_event, url: string) => {
|
||||
newWindow(url);
|
||||
});
|
||||
|
||||
|
||||
26
src/main/util/notification.ts
Normal file
26
src/main/util/notification.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Notification, type NotificationConstructorOptions } from "electron";
|
||||
import log from "electron-log/main";
|
||||
|
||||
function createNotification(
|
||||
options: NotificationConstructorOptions,
|
||||
context = options.title,
|
||||
): Notification {
|
||||
const notification = new Notification(options);
|
||||
|
||||
notification.on("failed", (_event, error) => {
|
||||
log.warn(`Notification failed${context ? ` (${context})` : ""}:`, error);
|
||||
});
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
function showNotification(
|
||||
options: NotificationConstructorOptions,
|
||||
context?: string,
|
||||
): Notification {
|
||||
const notification = createNotification(options, context);
|
||||
notification.show();
|
||||
return notification;
|
||||
}
|
||||
|
||||
export { createNotification, showNotification };
|
||||
@@ -1,5 +1,4 @@
|
||||
import chokidar, { FSWatcher } from "chokidar";
|
||||
import { Notification } from "electron";
|
||||
import log from "electron-log/main";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
@@ -8,6 +7,7 @@ import ipcTypes from "../../util/ipcTypes.json";
|
||||
import ImportJob from "../decoder/decoder";
|
||||
import store from "../store/store";
|
||||
import getMainWindow from "../../util/getMainWindow";
|
||||
import { showNotification } from "../util/notification";
|
||||
import { setWatcherTrayStatus } from "../util/trayStatus";
|
||||
let watcher: FSWatcher | null;
|
||||
let watcherReady = false;
|
||||
@@ -36,11 +36,11 @@ async function StartWatcher(
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
if (notifyOnNoPaths) {
|
||||
new Notification({
|
||||
showNotification({
|
||||
//TODO: Add Translations
|
||||
title: "Watcher cannot start",
|
||||
body: "Please set the appropriate file paths and try again.",
|
||||
}).show();
|
||||
});
|
||||
}
|
||||
log.warn("Cannot start watcher. No valid file paths set.", {
|
||||
configuredFilePaths,
|
||||
@@ -139,10 +139,10 @@ function onWatcherReady(): void {
|
||||
if (watcher) {
|
||||
const mainWindow = getMainWindow();
|
||||
watcherReady = true;
|
||||
new Notification({
|
||||
showNotification({
|
||||
title: "Watcher Started",
|
||||
body: "Newly exported estimates will be automatically uploaded.",
|
||||
}).show();
|
||||
});
|
||||
log.info("Confirmed watched paths:", watcher.getWatched());
|
||||
setWatcherTrayStatus(true);
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.started);
|
||||
@@ -160,10 +160,10 @@ async function StopWatcher(): Promise<boolean> {
|
||||
setWatcherTrayStatus(false);
|
||||
mainWindow?.webContents.send(ipcTypes.toRenderer.watcher.stopped);
|
||||
|
||||
new Notification({
|
||||
showNotification({
|
||||
title: "Watcher Stopped",
|
||||
body: "Estimates will not be automatically uploaded.",
|
||||
}).show();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import log from "electron-log/renderer";
|
||||
import type { RootState } from "./redux-store";
|
||||
|
||||
interface AppState {
|
||||
export interface AppState {
|
||||
value: number;
|
||||
watcher: {
|
||||
started: boolean;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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 type AppDispatch = typeof store.dispatch;
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); // Ex
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
export const useAppStore: () => AppStore = useStore;
|
||||
|
||||
@@ -39,7 +39,10 @@ const useCountDown = (
|
||||
interval,
|
||||
timer.current.timeLeft || Infinity,
|
||||
);
|
||||
if (timer.current.lastInterval && ts - timer.current.lastInterval >= localInterval) {
|
||||
if (
|
||||
timer.current.lastInterval &&
|
||||
ts - timer.current.lastInterval >= localInterval
|
||||
) {
|
||||
timer.current.lastInterval += localInterval;
|
||||
setTimeLeft((timeLeft) => {
|
||||
timer.current.timeLeft = timeLeft - localInterval;
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
// uri: import.meta.env.VITE_GRAPHQL_URL,
|
||||
// });
|
||||
|
||||
const middlewares = [];
|
||||
const middlewares: ApolloLink[] = [];
|
||||
|
||||
const client: ApolloClient<any> = new ApolloClient({
|
||||
const client: ApolloClient = new ApolloClient({
|
||||
link: ApolloLink.from(middlewares),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
@@ -21,9 +21,6 @@ const client: ApolloClient<any> = new ApolloClient({
|
||||
query: {
|
||||
fetchPolicy: "network-only",
|
||||
},
|
||||
mutate: {
|
||||
errorPolicy: "none",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user