Added log rocket + analytics to ensure functionality

This commit is contained in:
Patrick Fic
2020-10-22 12:38:33 -07:00
parent 295b51267b
commit ad7cbb308b
26 changed files with 434 additions and 134 deletions

View File

@@ -5,18 +5,24 @@ import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { DELETE_JOB } from "../../../graphql/jobs.queries";
import ipcTypes from "../../../ipc.types";
import { setSelectedJobId } from "../../../redux/application/application.actions";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
setSelectedJobId: (id) => dispatch(setSelectedJobId(id)),
});
export function DeleteJobAtom({ setSelectedJobId, jobId }) {
const [deleteJob] = useMutation(DELETE_JOB);
const [loading, setLoading] = useState(false);
const handleDelete = async () => {
setLoading(true);
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "DELETE_JOB",
});
const result = await deleteJob({
variables: { jobId: jobId },
});

View File

@@ -2,13 +2,20 @@ import { useMutation } from "@apollo/client";
import { message, Switch } from "antd";
import React, { useState } from "react";
import { UPDATE_JOB_LINE } from "../../../graphql/joblines.queries";
const { log } = window;
import ipcTypes from "../../../ipc.types";
const { log, ipcRenderer } = window;
export default function IgnoreJobLineAtom({ ignore, lineId }) {
export default function IgnoreJobLineAtom({ ignore, lineId, line_desc }) {
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const [loading, setLoading] = useState(false);
const handleChange = async (checked) => {
setLoading(true);
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "TOGGLE_IGNORE_LINE",
line_desc: line_desc,
ignore: checked,
});
const result = await updateJobLine({
variables: { lineId: lineId, line: { ignore: checked } },
});

View File

@@ -5,7 +5,10 @@ import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB } from "../../../graphql/jobs.queries";
import ipcTypes from "../../../ipc.types";
import { selectBodyshop } from "../../../redux/user/user.selectors";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
@@ -15,11 +18,17 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(JobGroupMolecule);
export function JobGroupMolecule({ bodyshop, jobId, group }) {
export function JobGroupMolecule({ bodyshop, jobId, group, job }) {
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(UPDATE_JOB);
const handleMenuClick = async (value) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "CHANGE_VEHICLE_GROUP",
vehicle: `${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type})`,
oldGroup: group,
newGroup: value.key,
});
setLoading(true);
const result = await updateJob({
variables: { jobId: jobId, job: { group: value.key } },

View File

@@ -21,7 +21,7 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
<CurrencyFormatterAtom>{job.clm_total}</CurrencyFormatterAtom>
</Descriptions.Item>
<Descriptions.Item label="Group">
<JobGroupMolecule jobId={job.id} group={job.group} />
<JobGroupMolecule jobId={job.id} group={job.group} job={job} />
</Descriptions.Item>
<Descriptions.Item label="Age">{job.v_age}</Descriptions.Item>
<Descriptions.Item label="Close Date">

View File

@@ -1,11 +1,14 @@
import { Input, Table } from "antd";
import React, { useState } from "react";
import ipcTypes from "../../../ipc.types";
import { alphaSort } from "../../../util/sorters";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
import IgnoreJobLine from "../../atoms/ignore-job-line/ignore-job-line.atom";
import partTypeConverterAtom from "../../atoms/part-type-converter/part-type-converter.atom";
import PriceDiffPcFormatterAtom from "../../atoms/price-diff-pc-formatter/price-diff-pc-formatter.atom";
const { ipcRenderer } = window;
export default function JobLinesTableMolecule({ loading, job }) {
const [searchText, setSearchText] = useState("");
@@ -92,7 +95,11 @@ export default function JobLinesTableMolecule({ loading, job }) {
],
onFilter: (value, record) => value === record.ignore,
render: (text, record) => (
<IgnoreJobLine lineId={record.id} ignore={record.ignore} />
<IgnoreJobLine
lineId={record.id}
ignore={record.ignore}
line_desc={record.line_desc}
/>
),
},
];
@@ -111,6 +118,10 @@ export default function JobLinesTableMolecule({ loading, job }) {
<Input.Search
placeholder="Search"
onSearch={(val) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "JOB_LINES_SEARCH",
query: val,
});
setSearchText(val);
}}
enterButton

View File

@@ -1,10 +1,17 @@
import { SearchOutlined } from "@ant-design/icons";
import { Button, DatePicker, Form, Input } from "antd";
import React from "react";
import ipcTypes from "../../../ipc.types";
const { ipcRenderer } = window;
export default function JobsSearchFieldsMolecule({ callSearchQuery }) {
const [form] = Form.useForm();
const handleFinish = (values) => {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "SEARCH_JOBS",
query: values.search,
datesIncluded: !!values.dateRange,
});
callSearchQuery({
variables: {
search: values.search || "",

View File

@@ -18,7 +18,6 @@ export function ReportingDatesMolecule({ queryReportingData }) {
const [form] = Form.useForm();
const handleFinish = (values) => {
console.log("values", values);
queryReportingData({
startDate: values.dateRange[0],
endDate: values.dateRange[1],

View File

@@ -9,7 +9,6 @@ export default function ShopSettingsFormMolecule({ form, saveLoading }) {
form.getFieldValue("groups") || []
);
const handleBlur = () => {
console.log(form.getFieldValue("groups") || []);
setGroupOptions(form.getFieldValue("groups") || []);
};

View File

@@ -4,10 +4,11 @@ import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../../graphql/bodyshop.queries";
import ipcTypes from "../../../ipc.types";
import { setBodyshop } from "../../../redux/user/user.actions";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import ShopSettingsFormMolecule from "../../molecules/shop-settings-form/shop-settings-form.molecule";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
@@ -15,10 +16,6 @@ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
setBodyshop: (shop) => dispatch(setBodyshop(shop)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ShopSettingsOrganism);
export function ShopSettingsOrganism({ setBodyshop }) {
const { loading, error, data } = useQuery(QUERY_BODYSHOP);
@@ -32,6 +29,9 @@ export function ShopSettingsOrganism({ setBodyshop }) {
const handleFinish = async (values) => {
setSaveLoading(true);
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "UPDATE_SHOP_DETAILS",
});
const result = await updateBodyshop({
variables: { id: data.bodyshops[0].id, shop: values },
@@ -70,3 +70,8 @@ export function ShopSettingsOrganism({ setBodyshop }) {
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ShopSettingsOrganism);

View File

@@ -5,8 +5,10 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ImEXOnlineLogo from "../../../assets/logo192.png";
import { emailSignInStart } from "../../../redux/user/user.actions";
import { sendPasswordReset } from "../../../redux/user/user.actions";
import {
selectLoginLoading,
selectPasswordReset,
selectSignInError,
} from "../../../redux/user/user.selectors";
import "./sign-in.page.styles.scss";
@@ -14,20 +16,33 @@ import "./sign-in.page.styles.scss";
const mapStateToProps = createStructuredSelector({
signInError: selectSignInError,
loginLoading: selectLoginLoading,
passwordReset: selectPasswordReset,
});
const mapDispatchToProps = (dispatch) => ({
emailSignInStart: (email, password) =>
dispatch(emailSignInStart({ email, password })),
sendPasswordReset: (email) => dispatch(sendPasswordReset(email)),
});
export function SignInPage({ emailSignInStart, signInError, loginLoading }) {
export function SignInPage({
emailSignInStart,
signInError,
loginLoading,
sendPasswordReset,
passwordReset,
}) {
const handleFinish = (values) => {
const { email, password } = values;
emailSignInStart(email, password);
};
const [form] = Form.useForm();
const handleReset = () => {
const email = form.getFieldValue("email");
sendPasswordReset(email);
};
return (
<div className="login-container">
<div className="login-logo-container">
@@ -35,10 +50,16 @@ export function SignInPage({ emailSignInStart, signInError, loginLoading }) {
<Typography.Title>ImEX RPS</Typography.Title>
</div>
<Form onFinish={handleFinish} form={form} size="large">
<Form.Item name="email" rules={[{ required: true }]}>
<Form.Item
name="email"
rules={[{ required: true, message: "Please enter a valid email." }]}
>
<Input prefix={<UserOutlined />} placeholder="Email" />
</Form.Item>
<Form.Item name="password" rules={[{ required: true }]}>
<Form.Item
name="password"
rules={[{ required: true, message: "Please enter your password." }]}
>
<Input
prefix={<LockOutlined />}
type="password"
@@ -46,7 +67,7 @@ export function SignInPage({ emailSignInStart, signInError, loginLoading }) {
/>
</Form.Item>
{signInError ? (
<Alert type="error" message={signInError.message} />
<Alert type="error" message={signInError.messagePretty} />
) : null}
<Button
className="login-btn"
@@ -56,7 +77,21 @@ export function SignInPage({ emailSignInStart, signInError, loginLoading }) {
>
Login
</Button>
<Form.Item shouldUpdate>
{() => {
return (
<Button
className="login-btn"
disabled={!form.getFieldValue("email")}
onClick={handleReset}
>
Reset Password
</Button>
);
}}
</Form.Item>
</Form>
{passwordReset.error && <div>{passwordReset.error}</div>}
</div>
);
}

View File

@@ -1,4 +1,5 @@
import "antd/dist/antd.css";
import LogRocket from "logrocket";
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
@@ -8,6 +9,7 @@ import App from "./App/App";
import "./index.css";
import { persistor, store } from "./redux/store";
require("dotenv").config();
LogRocket.init("imex/rps");
ReactDOM.render(
<Provider store={store}>

View File

@@ -7,6 +7,8 @@ exports.default = {
app: {
toMain: {
setAcceptableInsCoNm: "setAcceptableInsCoNm",
setUserName: "setUserName",
track: "analytics_track",
},
},
store: {

View File

@@ -5,7 +5,7 @@ import client from "../graphql/GraphQLClient";
import {
INSERT_NEW_JOB,
QUERY_JOB_BY_CLM_NO,
UPDATE_JOB,
UPDATE_JOB
} from "../graphql/jobs.queries";
import { QUERY_GROUPS_BY_MAKE_TYPE } from "../graphql/veh_group.queries";
import { store } from "../redux/store";
@@ -15,8 +15,6 @@ export async function UpsertEstimate(job) {
const shopId = store.getState().user.bodyshop.id;
logger.info("Beginning Upserting job from Renderer.");
const parsedYr = parseInt(job.v_model_yr);
console.log("UpsertEstimate -> parsedYr", parsedYr);
logger.info(
moment(job.loss_date).year() -
(parsedYr >= 0 ? 2000 + parsedYr : 1900 + parsedYr)
@@ -50,13 +48,11 @@ export async function UpsertEstimate(job) {
});
delete job.joblines;
const updatedJob = await client.mutate({
await client.mutate({
mutation: UPDATE_JOB,
variables: { jobId: existingJobs.data.jobs[0].id, job: job },
});
logger.info("Job updated succesfully.");
console.log("UpsertEstimate -> updatedJob", updatedJob);
} else {
logger.info("Attemping to insert job record.");

View File

@@ -1,4 +1,4 @@
import { all, call, takeLatest, select, put } from "redux-saga/effects";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import GetJobTarget from "../../util/GetJobTarget";
import { setSelectedJobTargetPcSuccess } from "./application.actions";
import ApplicationActionTypes from "./application.types";

View File

@@ -1,20 +1,21 @@
import { all, call, takeLatest, select, put } from "redux-saga/effects";
import Dinero from "dinero.js";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import client from "../../graphql/GraphQLClient";
import { REPORTING_GET_JOBS } from "../../graphql/reporting.queries";
import ipcTypes from "../../ipc.types";
import {
CalculateJobRpsDollars,
CalculateJobRpsPc
} from "../../util/CalculateJobRps";
import GetJobTarget from "../../util/GetJobTarget";
import {
calculateScorecard,
setReportingData,
setScoreCard,
setScoreCard
} from "./reporting.actions";
import ReportingApplicationTypes from "./reporting.types";
import client from "../../graphql/GraphQLClient";
import { REPORTING_GET_JOBS } from "../../graphql/reporting.queries";
import Dinero from "dinero.js";
import {
CalculateJobRpsDollars,
CalculateJobRpsPc,
} from "../../util/CalculateJobRps";
import GetJobTarget from "../../util/GetJobTarget";
const { log } = window;
const { log, ipcRenderer } = window;
export function* onQueryReportData() {
yield takeLatest(
@@ -52,71 +53,81 @@ export function* onCalculateScoreCard() {
);
}
export function* handleCalculateScoreCard({ payload: jobs }) {
console.log("jobs", jobs);
const targets = yield select((state) => state.user.bodyshop.targets);
try {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "CALCULATE_SCORECARD",
});
const scoreCard = {
shopRpsTotalDollars: Dinero(),
shopRpsExpectedDollars: Dinero(),
varianceDollars: null,
variancePc: 0,
allJobsSumDbPrice: Dinero(),
allJobsSumActPrice: Dinero(),
currentRpsPc: 0,
targetRpsPc: 0,
};
const targets = yield select((state) => state.user.bodyshop.targets);
//Get the RPS on a per job basis.
jobs = jobs.map((job) => {
const { actPriceSum, jobRpsDollars } = CalculateJobRpsDollars(job, true);
const { dbPriceSum, jobRpsPc } = CalculateJobRpsPc(
job,
jobRpsDollars,
true
);
const jobTarget = GetJobTarget(job.group, job.v_age, targets);
scoreCard.shopRpsTotalDollars = scoreCard.shopRpsTotalDollars.add(
jobRpsDollars
);
const expectedRpsDollars = dbPriceSum.percentage(jobTarget * 100);
scoreCard.shopRpsExpectedDollars = scoreCard.shopRpsExpectedDollars.add(
expectedRpsDollars
);
scoreCard.allJobsSumDbPrice = scoreCard.allJobsSumDbPrice.add(dbPriceSum);
scoreCard.allJobsSumActPrice = scoreCard.allJobsSumActPrice.add(
actPriceSum
);
//sum db price * percentage expected.
return {
...job,
actPriceSum,
jobRpsDollars,
dbPriceSum,
jobRpsPc,
jobTarget,
expectedRpsDollars,
const scoreCard = {
shopRpsTotalDollars: Dinero(),
shopRpsExpectedDollars: Dinero(),
varianceDollars: null,
variancePc: 0,
allJobsSumDbPrice: Dinero(),
allJobsSumActPrice: Dinero(),
currentRpsPc: 0,
targetRpsPc: 0,
};
});
scoreCard.varianceDollars = scoreCard.shopRpsTotalDollars.subtract(
scoreCard.shopRpsExpectedDollars
);
//Get the RPS on a per job basis.
jobs = jobs.map((job) => {
const { actPriceSum, jobRpsDollars } = CalculateJobRpsDollars(job, true);
const { dbPriceSum, jobRpsPc } = CalculateJobRpsPc(
job,
jobRpsDollars,
true
);
const jobTarget = GetJobTarget(job.group, job.v_age, targets);
scoreCard.shopRpsTotalDollars = scoreCard.shopRpsTotalDollars.add(
jobRpsDollars
);
const expectedRpsDollars = dbPriceSum.percentage(jobTarget * 100);
scoreCard.shopRpsExpectedDollars = scoreCard.shopRpsExpectedDollars.add(
expectedRpsDollars
);
scoreCard.variancePc =
scoreCard.varianceDollars.getAmount() /
scoreCard.shopRpsExpectedDollars.getAmount();
scoreCard.allJobsSumDbPrice = scoreCard.allJobsSumDbPrice.add(dbPriceSum);
scoreCard.allJobsSumActPrice = scoreCard.allJobsSumActPrice.add(
actPriceSum
);
scoreCard.currentRpsPc =
scoreCard.shopRpsTotalDollars.getAmount() /
scoreCard.allJobsSumDbPrice.getAmount();
scoreCard.targetRpsPc =
scoreCard.shopRpsExpectedDollars.getAmount() /
scoreCard.allJobsSumDbPrice.getAmount();
//Set the data.
yield put(setScoreCard(scoreCard));
yield put(setReportingData(jobs));
//sum db price * percentage expected.
return {
...job,
actPriceSum,
jobRpsDollars,
dbPriceSum,
jobRpsPc,
jobTarget,
expectedRpsDollars,
};
});
scoreCard.varianceDollars = scoreCard.shopRpsTotalDollars.subtract(
scoreCard.shopRpsExpectedDollars
);
scoreCard.variancePc =
scoreCard.varianceDollars.getAmount() /
scoreCard.shopRpsExpectedDollars.getAmount();
scoreCard.currentRpsPc =
scoreCard.shopRpsTotalDollars.getAmount() /
scoreCard.allJobsSumDbPrice.getAmount();
scoreCard.targetRpsPc =
scoreCard.shopRpsExpectedDollars.getAmount() /
scoreCard.allJobsSumDbPrice.getAmount();
//Set the data.
yield put(setScoreCard(scoreCard));
yield put(setReportingData(jobs));
} catch (error) {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "CALCULATE_SCORE_CARD_ERROR",
error: error,
});
}
}
export function* reportingSagas() {

View File

@@ -47,7 +47,7 @@ const userReducer = (state = INITIAL_STATE, action) => {
return {
...state,
currentUser: action.payload,
loingLoading: false,
loginLoading: false,
error: null,
};
case UserActionTypes.SIGN_OUT_SUCCESS:

View File

@@ -1,3 +1,5 @@
import { message } from "antd";
import LogRocket from "logrocket";
import { all, call, put, takeLatest } from "redux-saga/effects";
import {
auth,
@@ -18,8 +20,6 @@ import {
signOutSuccess,
unauthorizedUser,
updateUserDetailsSuccess,
validatePasswordResetFailure,
validatePasswordResetSuccess,
} from "./user.actions";
import UserActionTypes from "./user.types";
@@ -30,6 +30,10 @@ export function* onEmailSignInStart() {
}
export function* signInWithEmail({ payload: { email, password } }) {
try {
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "SIGN_IN_ATTEMPT",
email: email,
});
const { user } = yield auth.signInWithEmailAndPassword(email, password);
const result = yield client.mutate({
@@ -50,13 +54,16 @@ export function* signInWithEmail({ payload: { email, password } }) {
yield put(signInFailure(JSON.stringify(result.errors)));
}
} catch (error) {
yield put(signInFailure(error));
yield put(
signInFailure({ ...error, messagePretty: ErrorFormatter(error.code) })
);
}
}
export function* onCheckUserSession() {
yield takeLatest(UserActionTypes.CHECK_USER_SESSION, isUserAuthenticated);
}
export function* isUserAuthenticated() {
try {
const user = yield getCurrentUser();
@@ -78,9 +85,11 @@ export function* isUserAuthenticated() {
yield put(signInFailure(error));
}
}
export function* onSignOutStart() {
yield takeLatest(UserActionTypes.SIGN_OUT_START, signOutStart);
}
export function* signOutStart() {
try {
ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.stop);
@@ -112,7 +121,15 @@ export function* onSignInSuccess() {
export function* signInSuccessSaga({ payload }) {
//Query for the Correct Bodyshop
ipcRenderer.send(ipcTypes.default.app.toMain.setUserName, payload.email);
LogRocket.identify(payload.email, {
email: payload.email,
});
ipcRenderer.send(ipcTypes.default.app.toMain.track, {
event: "SIGN_IN_SUCCESS",
email: payload.email,
});
const shop = yield client.query({ query: QUERY_BODYSHOP });
if (shop.data.bodyshops.length > 0) {
yield put(setBodyshop(shop.data.bodyshops[0]));
@@ -138,32 +155,14 @@ export function* onSendPasswordResetStart() {
}
export function* sendPasswordResetEmail({ payload }) {
try {
yield auth.sendPasswordResetEmail(payload, {
url: "https://imex.online/passwordreset",
});
yield auth.sendPasswordResetEmail(payload);
yield put(sendPasswordResetSuccess());
message.success("Password reset sent succesfully.");
} catch (error) {
yield put(sendPasswordResetFailure(error.message));
}
}
export function* onValidatePasswordResetStart() {
yield takeLatest(
UserActionTypes.VALIDATE_PASSWORD_RESET_START,
validatePasswordResetStart
);
}
export function* validatePasswordResetStart({ payload: { password, code } }) {
try {
yield auth.confirmPasswordReset(code, password);
yield put(validatePasswordResetSuccess());
} catch (error) {
console.log("function*validatePasswordResetStart -> error", error);
yield put(validatePasswordResetFailure(error.message));
}
}
export function* userSagas() {
yield all([
call(onEmailSignInStart),
@@ -172,6 +171,18 @@ export function* userSagas() {
call(onUpdateUserDetails),
call(onSignInSuccess),
call(onSendPasswordResetStart),
call(onValidatePasswordResetStart),
]);
}
const ErrorFormatter = (code) => {
switch (code) {
case "auth/invalid-email":
return "Please enter a valid email.";
case "auth/user-not-found":
return "A user does not exist with that email.";
case "auth/wrong-password":
return "The email or password is incorrect.";
default:
return code;
}
};