Add basic acknolwedgmenet to notifications.

This commit is contained in:
Patrick Fic
2022-11-22 11:27:37 -08:00
parent 66626472a7
commit 7bf838ddb5
30 changed files with 344 additions and 38 deletions

5
deployment.md Normal file
View File

@@ -0,0 +1,5 @@
Deployment:
Change version to 2.11.2
Update VARS to HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE = true

View File

@@ -1,34 +1,34 @@
const { ipcMain } = require("electron");
const { app } = require("electron");
//const { app } = require("electron");
const log = require("electron-log");
const Nucleus = require("nucleus-nodejs");
//const Nucleus = require("nucleus-nodejs");
const { default: ipcTypes } = require("../src/ipc.types");
Nucleus.init("5f91b569b95bac34eefdb63a", {
disableInDev: true,
debug: false,
version: app.getVersion(),
});
// Nucleus.init("5f91b569b95bac34eefdb63a", {
// disableInDev: true,
// debug: false,
// version: app.getVersion(),
// });
Nucleus.setProps({
version: app.getVersion(),
});
// Nucleus.setProps({
// version: app.getVersion(),
// });
Nucleus.onError = (type, err) => {
log.error(err);
// type will either be uncaughtException, unhandledRejection or windowError
};
// Nucleus.onError = (type, err) => {
// log.error(err);
// // type will either be uncaughtException, unhandledRejection or windowError
// };
ipcMain.on(ipcTypes.app.toMain.setUserName, (event, userName) => {
Nucleus.setUserId(userName);
Nucleus.appStarted();
// Nucleus.setUserId(userName);
// Nucleus.appStarted();
});
ipcMain.on(ipcTypes.app.toMain.track, (e, args) => {
log.log("NUCLEUS Event", args);
const { event, ...eventDetails } = args;
// const { event, ...eventDetails } = args;
try {
Nucleus.track(event, eventDetails);
//// Nucleus.track(event, eventDetails);
} catch (error) {
log.error(error);
}

View File

@@ -8,7 +8,7 @@ const ipcTypes = require("../../src/ipc.types");
const {
NewNotification,
} = require("../notification-wrapper/notification-wrapper");
const Nucleus = require("nucleus-nodejs");
//const Nucleus = require("nucleus-nodejs");
async function ImportJob(path) {
const b = BrowserWindow.getAllWindows()[0];
@@ -27,7 +27,7 @@ async function ImportJob(path) {
});
} else {
log.info(`Ignored job. ${newJob.ERROR}`);
Nucleus.track("IGNORE_JOB", { reason: newJob.ERROR });
// Nucleus.track("IGNORE_JOB", { reason: newJob.ERROR });
NewNotification({
title: "Job Ignored",
body: newJob.ERROR,

View File

@@ -1,5 +1,5 @@
const { ipcMain } = require("electron");
const Nucleus = require("nucleus-nodejs");
//const Nucleus = require("nucleus-nodejs");
const ipcTypes = require("../../src/ipc.types");
const { ImportJob } = require("../decoder/decoder");
const { GetListOfEstimates, DeleteAllEms } = require("./file-scan");
@@ -18,7 +18,7 @@ ipcMain.on(
ipcMain.on(
ipcTypes.default.fileScan.toMain.importJob,
async (event, filePath) => {
Nucleus.track("IMPORT_JOB_FROM_SCAN");
// Nucleus.track("IMPORT_JOB_FROM_SCAN");
await ImportJob(filePath);
}
);
@@ -26,7 +26,7 @@ ipcMain.on(
ipcMain.on(
ipcTypes.default.fileScan.toMain.deleteAllEms,
async (event, filePath) => {
Nucleus.track("DELETE_ALLEMS");
// Nucleus.track("DELETE_ALLEMS");
await DeleteAllEms();
const ret = await GetListOfEstimates();
event.reply(

View File

@@ -3,13 +3,11 @@ const fs = require("fs");
const { store } = require("../electron-store");
const log = require("electron-log");
const fsPromises = fs.promises;
const _ = require("lodash");
const { DecodeEstimate } = require("../decoder/decoder");
const Nucleus = require("nucleus-nodejs");
const { format } = require("path");
//const Nucleus = require("nucleus-nodejs");
async function GetListOfEstimates() {
Nucleus.track("SCAN_ALL_ESTIMATES");
// Nucleus.track("SCAN_ALL_ESTIMATES");
log.info("Scanning all local estimates..");
const ListOfEnvFiles = await GetEnvFiles();
const ListOfSummarizedEstimates = await ReadAllEstimates(ListOfEnvFiles);

View File

@@ -8,7 +8,7 @@ const {
NewNotification,
} = require("../notification-wrapper/notification-wrapper");
const log = require("electron-log");
const Nucleus = require("nucleus-nodejs");
//const Nucleus = require("nucleus-nodejs");
var watcher;
async function StartWatcher() {
@@ -71,7 +71,7 @@ async function StartWatcher() {
log.error("Error in Watcher", error);
const b = BrowserWindow.getFocusedWindow();
b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.error, error);
Nucleus.track("WATCHER_ERROR", error);
// Nucleus.track("WATCHER_ERROR", error);
})
.on("ready", onWatcherReady)
.on("raw", function (event, path, details) {

View File

@@ -15,7 +15,7 @@ const { store } = require("./electron-store");
const { autoUpdater } = require("electron-updater");
const log = require("electron-log");
const Nucleus = require("nucleus-nodejs");
//const Nucleus = require("nucleus-nodejs");
require("./ipc-main-handler");
require("./analytics");
@@ -299,12 +299,12 @@ ipcMain.on(ipcTypes.app.toMain.checkForUpdates, (event, args) => {
});
ipcMain.on(ipcTypes.app.toMain.downloadUpdates, (event, args) => {
Nucleus.track("DOWNLOAD_UPDATE_FROM_RENDERER");
//Nucleus.track("DOWNLOAD_UPDATE_FROM_RENDERER");
autoUpdater.downloadUpdate();
});
ipcMain.on(ipcTypes.app.toMain.installUpdates, (event, args) => {
Nucleus.track("INSTALL_UPDATE_FROM_RENDERER");
//Nucleus.track("INSTALL_UPDATE_FROM_RENDERER");
const isSilent = true;
const isForceRunAfter = true;
autoUpdater.quitAndInstall(isSilent, isForceRunAfter);
@@ -316,7 +316,7 @@ autoUpdater.on("download-progress", (ev) => {
});
autoUpdater.on("update-downloaded", (ev, info) => {
Nucleus.track("UPDATE_DOWNLOADED", ev);
//Nucleus.track("UPDATE_DOWNLOADED", ev);
// if (process.env.NODE_ENV === "production") {
mainWindow.webContents.send(ipcTypes.app.toRenderer.downloadProgress, {
...ev,

View File

@@ -16,6 +16,13 @@ array_relationships:
table:
name: jobs
schema: public
- name: notifications
using:
foreign_key_constraint_on:
column: bodyshopid
table:
name: notifications
schema: public
select_permissions:
- role: user
permission:

View File

@@ -0,0 +1,43 @@
table:
name: notifications
schema: public
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
select_permissions:
- role: user
permission:
columns:
- acceptedat
- acceptedby
- bodyshopid
- created_at
- effectivedate
- html
- id
- requiresconfirmation
- requiresconfirmationby
- updated_at
filter:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- acceptedat
- acceptedby
filter:
_and:
- bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
- acceptedat:
_is_null: true
check: null

View File

@@ -3,5 +3,6 @@
- "!include public_groupings.yaml"
- "!include public_joblines.yaml"
- "!include public_jobs.yaml"
- "!include public_notifications.yaml"
- "!include public_users.yaml"
- "!include public_veh_groups.yaml"

View File

@@ -0,0 +1,35 @@
CREATE TABLE "public"."users"("email" text NOT NULL, "authid" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("email") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_users_updated_at"
BEFORE UPDATE ON "public"."users"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_users_updated_at" ON "public"."users"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE TABLE "public"."users"("email" text NOT NULL, "authid" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("email") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_users_updated_at"
BEFORE UPDATE ON "public"."users"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_users_updated_at" ON "public"."users"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@@ -0,0 +1 @@
DROP TABLE "public"."notifications";

View File

@@ -0,0 +1,17 @@
CREATE TABLE "public"."notifications" ("id" serial NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid, "effectivedate" timestamptz NOT NULL, "html" text NOT NULL, "acceptedat" timestamptz, "acceptedby" text, "requiresaacceptance" text NOT NULL DEFAULT 'false', PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE restrict ON DELETE restrict);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_notifications_updated_at"
BEFORE UPDATE ON "public"."notifications"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_notifications_updated_at" ON "public"."notifications"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@@ -0,0 +1,3 @@
alter table "public"."notifications" alter column "requiresaacceptance" set default ''false'::text';
alter table "public"."notifications" alter column "requiresaacceptance" drop not null;
alter table "public"."notifications" add column "requiresaacceptance" text;

View File

@@ -0,0 +1 @@
alter table "public"."notifications" drop column "requiresaacceptance" cascade;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."notifications" add column "requiresconfirmation" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."notifications" add column "requiresconfirmation" boolean
not null default 'false';

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."notifications" add column "requiresconfirmationby" timestamptz
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."notifications" add column "requiresconfirmationby" timestamptz
null;

View File

@@ -0,0 +1,70 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, Space } from "antd";
import React from "react";
import { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { ACCEPT_NOTIFICATION } from "../../../graphql/notification.queries";
import { checkForNotification } from "../../../redux/user/user.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
checkForNotification: () => dispatch(checkForNotification()),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(NotificationDisplayMolecule);
export function NotificationDisplayMolecule({
notification,
checkForNotification,
}) {
const [acceptNotification] = useMutation(ACCEPT_NOTIFICATION);
const [loading, setLoading] = useState(false);
const handleConfirm = async (values) => {
console.log("form submit", notification, values);
setLoading(true);
await acceptNotification({
variables: {
id: notification.id,
notification: { acceptedby: values.acceptedby, acceptedat: new Date() },
},
});
checkForNotification();
setLoading(false);
};
const handleDismiss = async () => {};
const [form] = Form.useForm();
return (
<Card>
<div dangerouslySetInnerHTML={{ __html: notification.html }} />
{notification.requiresconfirmation ? (
<Form form={form} onFinish={handleConfirm}>
<Space align="baseline">
<Form.Item
label="Acknowledged by"
name="acceptedby"
rules={[
{
required: true,
message:
"Please enter your name to acknowledge and agree to the notice above.",
},
]}
>
<Input />
</Form.Item>
<Button loading={loading} onClick={() => form.submit()}>
Acknowledge
</Button>
</Space>
</Form>
) : (
<Button onClick={handleDismiss}>Dismiss</Button>
)}
</Card>
);
}

View File

@@ -0,0 +1,30 @@
import { Modal } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectNotifications } from "../../../redux/user/user.selectors";
import NotificationDisplayMolecule from "../../molecules/notification-display/notification-display.molecule";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
notifications: selectNotifications,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(NotificationModal);
export function NotificationModal({ notifications }) {
if (!notifications || notifications.length === 0) return null;
return (
<Modal
open={notifications && notifications.length > 0}
closable={false}
footer={null}
>
{notifications &&
notifications.map((n) => (
<NotificationDisplayMolecule key={n.id} notification={n} />
))}
</Modal>
);
}

View File

@@ -7,6 +7,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import ReleaseNotes from "../../molecules/release-notes/release-notes.molecule";
import NotificationModalOrganism from "../../organisms/notification-modal/notification-modal.organism";
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
import UpdateManagerOrganism from "../../organisms/update-manager/update-manager.organism";
import JobsPage from "../jobs/jobs.page";
@@ -37,6 +38,7 @@ export function RoutesPage({ bodyshop }) {
</Layout.Sider>
<Layout style={{ background: "#fff" }}>
<Layout.Content style={{ marginLeft: "1rem", height: "100%" }}>
<NotificationModalOrganism />
<Routes>
<Route exact path="/settings" element={<SettingsPage />} />
<Route exact path="/reporting" element={<ReportingPage />} />

View File

@@ -16,7 +16,7 @@ export const UPDATE_SHOP = gql`
mutation UPDATE_SHOP($id: uuid, $shop: bodyshops_set_input!) {
update_bodyshops(where: { id: { _eq: $id } }, _set: $shop) {
returning {
id
id
shopname
targets
accepted_ins_co
@@ -26,3 +26,18 @@ export const UPDATE_SHOP = gql`
}
}
`;
export const QUERY_NOTIFICATIONS = gql`
query QUERY_NOTIFICATIONS($now: timestamptz) {
notifications(
where: { acceptedat: { _is_null: true }, effectivedate: { _lte: $now } }
) {
effectivedate
html
id
requiresconfirmation
requiresconfirmationby
updated_at
}
}
`;

View File

@@ -0,0 +1,18 @@
import gql from "graphql-tag";
export const ACCEPT_NOTIFICATION = gql`
mutation ACCEPT_NOTIFICATION(
$id: Int!
$notification: notifications_set_input
) {
update_notifications_by_pk(pk_columns: { id: $id }, _set: $notification) {
html
id
requiresconfirmation
requiresconfirmationby
updated_at
acceptedat
effectivedate
}
}
`;

View File

@@ -150,6 +150,7 @@
"UPLANDER",
"YUKON",
"YUKON DENALI",
"YUKON XL",
"EQUINOX LS",
"EQUINOX LT",
"EQUINOX PREMIER",
@@ -164,6 +165,7 @@
"RAV4 XLE HYBRID",
"HIGHLANDER",
"4RUNNER",
"SEQUOIA",
"PATHFINDER SE",
"PATHFINDER SL",

View File

@@ -85,3 +85,10 @@ export const validatePasswordResetFailure = (error) => ({
payload: error,
});
export const setNotification = (notificationObject) => ({
type: UserActionTypes.SET_NOTIFICATIONS,
payload: notificationObject,
});
export const checkForNotification = () => ({
type: UserActionTypes.CHECK_FOR_NOTIFICATION,
});

View File

@@ -15,10 +15,13 @@ const INITIAL_STATE = {
success: false,
},
loginLoading: false,
notifications: null,
};
const userReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case UserActionTypes.SET_NOTIFICATIONS:
return { ...state, notifications: action.payload };
case UserActionTypes.SET_SHOP_DETAILS:
return { ...state, bodyshop: action.payload };
case UserActionTypes.SET_LOCAL_FINGERPRINT:

View File

@@ -1,12 +1,16 @@
import { message } from "antd";
import { message, notification } from "antd";
import moment from "moment";
//import LogRocket from "logrocket";
import { all, call, put, takeLatest } from "redux-saga/effects";
import { all, call, put, takeLatest, delay } from "redux-saga/effects";
import {
auth,
getCurrentUser,
updateCurrentUser,
} from "../../firebase/firebase.utils";
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
import {
QUERY_BODYSHOP,
QUERY_NOTIFICATIONS,
} from "../../graphql/bodyshop.queries";
import client from "../../graphql/GraphQLClient";
import { UPSERT_USER } from "../../graphql/user.queries";
import ipcTypes from "../../ipc.types";
@@ -20,6 +24,8 @@ import {
signOutSuccess,
unauthorizedUser,
updateUserDetailsSuccess,
checkForNotification,
setNotification,
} from "./user.actions";
import UserActionTypes from "./user.types";
@@ -144,6 +150,8 @@ export function* signInSuccessSaga({ payload }) {
ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.start, {
startup: true,
});
yield put(checkForNotification());
//Check for notifications, and continue to check.
} else {
console.log("No bodyshop has been associated.");
yield put(setBodyshop(false));
@@ -153,6 +161,27 @@ export function* signInSuccessSaga({ payload }) {
// yield logImEXEvent("redux_sign_in_success");
}
export function* onCheckForNotification() {
yield takeLatest(
UserActionTypes.CHECK_FOR_NOTIFICATION,
checkForNotificationSaga
);
}
export function* checkForNotificationSaga() {
const {
data: { notifications },
} = yield client.query({
query: QUERY_NOTIFICATIONS,
variables: { now: moment() },
});
if (notifications) {
yield put(setNotification(notifications));
}
yield delay(4 * 60 * 60 * 1000);
yield put(checkForNotification());
}
export function* onSendPasswordResetStart() {
yield takeLatest(
UserActionTypes.SEND_PASSWORD_RESET_EMAIL_START,
@@ -181,6 +210,7 @@ export function* userSagas() {
call(onUpdateUserDetails),
call(onSignInSuccess),
call(onSendPasswordResetStart),
call(onCheckForNotification),
]);
}

View File

@@ -26,3 +26,7 @@ export const selectLoginLoading = createSelector(
[selectUser],
(user) => user.loginLoading
);
export const selectNotifications = createSelector(
[selectUser],
(user) => user.notifications
);

View File

@@ -27,5 +27,7 @@ const UserActionTypes = {
VALIDATE_PASSWORD_RESET_SUCCESS: "VALIDATE_PASSWORD_RESET_SUCCESS",
VALIDATE_PASSWORD_RESET_FAILURE: "VALIDATE_PASSWORD_RESET_FAILURE",
SET_AUTH_LEVEL: "SET_AUTH_LEVEL",
CHECK_FOR_NOTIFICATION: "CHECK_FOR_NOTIFICATION",
SET_NOTIFICATIONS: "SET_NOTIFICATIONS",
};
export default UserActionTypes;
export default UserActionTypes;