Merged in v1.0.1 (pull request #2)

Prepare for V1.0.1
This commit is contained in:
Patrick Fic
2020-10-21 00:21:00 +00:00
62 changed files with 1304 additions and 141 deletions

View File

@@ -326,28 +326,27 @@ async function DecodeLinFile(extensionlessFilePath) {
}
);
})
.filter(
(jobline) =>
jobline.part_type &&
!jobline.db_ref.startsWith("900") &&
!jobline.db_ref.toLowerCase().startsWith("urethane") &&
!jobline.db_ref.toLowerCase().startsWith("wheel") &&
!jobline.db_ref.toLowerCase().startsWith("hazardous") &&
!jobline.db_ref.toLowerCase().startsWith("detail") &&
!jobline.db_ref.toLowerCase().startsWith("clean") &&
jobline.part_type.toUpperCase() !== "PAG" &&
jobline.part_type.toUpperCase() !== "PAS" &&
jobline.part_type.toUpperCase() !== "PASL" &&
jobline.part_type.toUpperCase() !== "PAE" &&
jobline.glass_flag === false
)
// .filter(
// (jobline) =>
// jobline.part_type &&
// !jobline.db_ref.startsWith("900") &&
// !jobline.line_desc.toLowerCase().startsWith("urethane") &&
// !jobline.line_desc.toLowerCase().startsWith("wheel") &&
// !jobline.line_desc.toLowerCase().startsWith("hazardous") &&
// !jobline.line_desc.toLowerCase().startsWith("detail") &&
// !jobline.line_desc.toLowerCase().startsWith("clean") &&
// jobline.part_type.toUpperCase() !== "PAG" &&
// jobline.part_type.toUpperCase() !== "PAS" &&
// jobline.part_type.toUpperCase() !== "PASL" &&
// jobline.part_type.toUpperCase() !== "PAE" &&
// jobline.glass_flag === false
// )
.map((jobline) => {
if (
(jobline.db_price === null || jobline.db_price === 0) &&
!!jobline.act_price &&
jobline.act_price > 0
) {
console.log(1, jobline.line_desc, jobline.db_price, jobline.act_price);
log.info(
"DB Price null/lower than act price",
jobline.line_desc,
@@ -368,10 +367,25 @@ async function DecodeLinFile(extensionlessFilePath) {
jobline.db_price,
jobline.act_price
);
console.log(2, jobline.line_desc, jobline.db_price, jobline.act_price);
jobline.db_price = jobline.act_price;
}
if (
!jobline.part_type ||
jobline.db_ref.startsWith("900") ||
jobline.line_desc.toLowerCase().startsWith("urethane") ||
jobline.line_desc.toLowerCase().startsWith("wheel") ||
jobline.line_desc.toLowerCase().startsWith("hazardous") ||
jobline.line_desc.toLowerCase().startsWith("detail") ||
jobline.line_desc.toLowerCase().startsWith("clean") ||
jobline.part_type.toUpperCase() === "PAG" ||
jobline.part_type.toUpperCase() === "PAS" ||
jobline.part_type.toUpperCase() === "PASL" ||
jobline.part_type.toUpperCase() === "PAE" ||
jobline.glass_flag === true
)
jobline.ignore = true;
delete jobline.glass_flag;
return jobline;
});

View File

@@ -1,5 +1,14 @@
const Store = require("electron-store");
const store = new Store({ defaults: { filePaths: [], accepted_ins_co: [] } });
const store = new Store({
defaults: {
filePaths: [],
accepted_ins_co: [],
polling: {
enabled: false,
pollingInterval: 100,
},
},
});
exports.store = store;

View File

@@ -7,35 +7,38 @@ const { store } = require("../electron-store");
const {
NewNotification,
} = require("../notification-wrapper/notification-wrapper");
const log = require("electron-log");
var watcher;
async function StartWatcher() {
const filePaths =
store.get("filePaths").map((fp) => path.join(fp, "**.[eE][nN][vV]")) || [];
console.log("StartWatcher -> filePaths", filePaths);
log.info("StartWatcher -> filePaths", filePaths);
log.info("Use polling? ", store.get("polling").enabled);
if (filePaths.length === 0) {
NewNotification({
title: "RPS Watcher cannot start",
body: "Please set the appropriate file paths and try again.",
}).show();
log.warn("Cannot start watcher. No file paths set.");
return [];
}
if (watcher) {
try {
console.log("Trying to close watcher - it already existed.");
log.info("Trying to close watcher - it already existed.");
await watcher.close();
console.log("Watcher closed successfully!");
log.info("Watcher closed successfully!");
} catch (error) {
console.log("Error trying to close Watcher.", error);
log.error("Error trying to close Watcher.", error);
}
}
watcher = chokidar.watch(filePaths, {
//ignored: /[\/\\]\./,
usePolling: store.get("polling").enabled,
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
@@ -88,7 +91,7 @@ function onWatcherReady() {
async function StopWatcher() {
await watcher.close();
console.log("Watcher stopped.");
log.info("Watcher stopped.");
const b = BrowserWindow.getAllWindows()[0];
b.webContents.send(ipcTypes.default.fileWatcher.toRenderer.stopSuccess);
NewNotification({
@@ -111,12 +114,13 @@ async function HandleNewFile(path) {
ipcTypes.default.estimate.toRenderer.estimateDecodeSuccess,
newJob
);
log.info(`Sent job for upload. ${newJob.clm_no}`);
NewNotification({
title: "Job Uploaded",
body: "A new job has been uploaded.",
}).show();
} else {
log.info(`Ignored job. ${newJob.ERROR}`);
NewNotification({
title: "Job Ignored",
body: newJob.ERROR,

View File

@@ -17,3 +17,20 @@ ipcMain.on("test", async (event, object) => {
ipcMain.on(ipcTypes.app.toMain.setAcceptableInsCoNm, (event, insCos) => {
store.set("accepted_ins_co", insCos);
});
ipcMain.on(ipcTypes.store.get, (event, key) => {
const val = store.get(key);
event.sender.send(ipcTypes.store.response, { [key]: val });
});
ipcMain.on(ipcTypes.store.set, (event, key, val) => {
store.set(key, val);
const st = store.get();
event.sender.send(ipcTypes.store.response, st);
});
ipcMain.on(ipcTypes.store.getAll, (event, obj) => {
const val = store.get();
event.sender.send(ipcTypes.store.response, val);
});

View File

@@ -23,11 +23,11 @@ log.info("App starting...");
// Conditionally include the dev tools installer to load React Dev Tools
let installExtension, REACT_DEVELOPER_TOOLS;
// if (isDev) {
// const devTools = require("electron-devtools-installer");
// installExtension = devTools.default;
// REACT_DEVELOPER_TOOLS = devTools.REACT_DEVELOPER_TOOLS;
// }
if (isDev) {
const devTools = require("electron-devtools-installer");
installExtension = devTools.default;
REACT_DEVELOPER_TOOLS = devTools.REACT_DEVELOPER_TOOLS;
}
var menu = Menu.buildFromTemplate([
{

View File

@@ -1,2 +1,3 @@
[1014/195617.530:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
[1015/081931.328:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
[1020/073641.000:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,18 @@
- args:
cascade: true
read_only: false
sql: "CREATE OR REPLACE FUNCTION public.search_jobs(search text, startdate date,
enddate date)\n RETURNS SETOF jobs\n LANGUAGE plpgsql\n STABLE\nAS $function$
BEGIN if search = '' then return query\nselect *\nfrom jobs j;\nelse\n\nif (startDate
is null) or (endDate is null) then \nreturn query\nSELECT *\nFROM jobs j2\nWHERE
\n\nownr_fn ILIKE '%' || search || '%'\n or ownr_ln ILIKE '%' || search ||
'%'\n \n or clm_no ILIKE '%' || search || '%'\nORDER BY \n clm_no ILIKE
'%' || search || '%'\n OR null,\n ownr_fn ILIKE '%' || search || '%'\n
\ OR NULL,\n ownr_ln ILIKE '%' || search || '%'\n OR NULL;\nelse \nreturn
query\nSELECT *\nFROM jobs j2\nWHERE \nclose_date between startDate and endDate
and close_date is not null and\n(\nownr_fn ILIKE '%' || search || '%'\n or
ownr_ln ILIKE '%' || search || '%'\n \n or clm_no ILIKE '%' || search ||
'%')\n\nORDER BY \n clm_no ILIKE '%' || search || '%'\n OR null,\n ownr_fn
ILIKE '%' || search || '%'\n OR NULL,\n ownr_ln ILIKE '%' || search ||
'%'\n OR NULL;\n\nend if;\n\n\nend if;\nEND $function$;"
type: run_sql

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,19 @@
- args:
cascade: true
read_only: false
sql: "CREATE OR REPLACE FUNCTION public.search_jobs(search text, startdate date,
enddate date)\n RETURNS SETOF jobs\n LANGUAGE plpgsql\n STABLE\nAS $function$\nBEGIN
if search = '' and ((startDate is null) or (endDate is null)) then return query\nselect
*\nfrom jobs j;\nelse\n\nif (startDate is null) or (endDate is null) then \nreturn
query\nSELECT *\nFROM jobs j2\nWHERE \n\nownr_fn ILIKE '%' || search || '%'\n
\ or ownr_ln ILIKE '%' || search || '%'\n \n or clm_no ILIKE '%' || search
|| '%'\nORDER BY \n clm_no ILIKE '%' || search || '%'\n OR null,\n ownr_fn
ILIKE '%' || search || '%'\n OR NULL,\n ownr_ln ILIKE '%' || search ||
'%'\n OR NULL;\nelse \nreturn query\nSELECT *\nFROM jobs j2\nWHERE \nclose_date
between startDate and endDate and close_date is not null and\n(\nownr_fn ILIKE
'%' || search || '%'\n or ownr_ln ILIKE '%' || search || '%'\n \n or
clm_no ILIKE '%' || search || '%')\n\nORDER BY \n clm_no ILIKE '%' || search
|| '%'\n OR null,\n ownr_fn ILIKE '%' || search || '%'\n OR NULL,\n
\ ownr_ln ILIKE '%' || search || '%'\n OR NULL;\n\nend if;\n\n\nend if;\nEND
$function$;"
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."joblines" DROP COLUMN "ignore";
type: run_sql

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."joblines" ADD COLUMN "ignore" boolean NOT NULL DEFAULT
false;
type: run_sql

View File

@@ -0,0 +1,39 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_insert_permission
- args:
permission:
backend_only: false
check:
job:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
columns:
- act_price
- created_at
- db_price
- db_ref
- id
- jobid
- line_desc
- line_ind
- line_no
- oem_partno
- part_qty
- part_type
- price_diff
- price_diff_pc
- unq_seq
- updated_at
set: {}
role: user
table:
name: joblines
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,40 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_insert_permission
- args:
permission:
backend_only: false
check:
job:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
columns:
- act_price
- created_at
- db_price
- db_ref
- id
- ignore
- jobid
- line_desc
- line_ind
- line_no
- oem_partno
- part_qty
- part_type
- price_diff
- price_diff_pc
- unq_seq
- updated_at
set: {}
role: user
table:
name: joblines
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,39 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- act_price
- created_at
- db_price
- db_ref
- id
- jobid
- line_desc
- line_ind
- line_no
- oem_partno
- part_qty
- part_type
- price_diff
- price_diff_pc
- unq_seq
- updated_at
computed_fields: []
filter:
job:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: joblines
schema: public
type: create_select_permission

View File

@@ -0,0 +1,40 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- act_price
- created_at
- db_price
- db_ref
- id
- ignore
- jobid
- line_desc
- line_ind
- line_no
- oem_partno
- part_qty
- part_type
- price_diff
- price_diff_pc
- unq_seq
- updated_at
computed_fields: []
filter:
job:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: joblines
schema: public
type: create_select_permission

View File

@@ -0,0 +1,38 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_update_permission
- args:
permission:
columns:
- act_price
- created_at
- db_price
- db_ref
- id
- jobid
- line_desc
- line_ind
- line_no
- oem_partno
- part_qty
- part_type
- price_diff
- price_diff_pc
- unq_seq
- updated_at
filter:
job:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
set: {}
role: user
table:
name: joblines
schema: public
type: create_update_permission

View File

@@ -0,0 +1,39 @@
- args:
role: user
table:
name: joblines
schema: public
type: drop_update_permission
- args:
permission:
columns:
- act_price
- created_at
- db_price
- db_ref
- id
- ignore
- jobid
- line_desc
- line_ind
- line_no
- oem_partno
- part_qty
- part_type
- price_diff
- price_diff_pc
- unq_seq
- updated_at
filter:
job:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
set: {}
role: user
table:
name: joblines
schema: public
type: create_update_permission

View File

@@ -89,6 +89,7 @@ tables:
- db_price
- db_ref
- id
- ignore
- jobid
- line_desc
- line_ind
@@ -110,6 +111,7 @@ tables:
- db_price
- db_ref
- id
- ignore
- jobid
- line_desc
- line_ind
@@ -137,6 +139,7 @@ tables:
- db_price
- db_ref
- id
- ignore
- jobid
- line_desc
- line_ind

View File

@@ -3,7 +3,7 @@
"productName": "ImEX RPS",
"author": "ImEX Systems Inc. <support@thinkimex.com>",
"description": "ImEX RPS",
"version": "1.0.0",
"version": "1.0.1",
"main": "electron/main.js",
"homepage": "./",
"dependencies": {

View File

@@ -0,0 +1,26 @@
import React from "react";
export default function DataLabel({
label,
hideIfNull,
children,
vertical,
visible = true,
...props
}) {
if (!visible || (hideIfNull && !!!children)) return null;
return (
<div {...props}>
<div
style={{
display: vertical ? "block" : "inline-block",
marginRight: ".2rem",
}}
>{`${label}: `}</div>
<div style={{ display: vertical ? "block" : "inline-block" }}>
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
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;
export default function IgnoreJobLineAtom({ ignore, lineId }) {
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
const [loading, setLoading] = useState(false);
const handleChange = async (checked) => {
setLoading(true);
const result = await updateJobLine({
variables: { lineId: lineId, line: { ignore: checked } },
});
if (result.errors) {
message.error("Error updating line.");
log.error("Error updating job.", result.errors);
} else {
}
setLoading(false);
};
return <Switch checked={ignore} onChange={handleChange} loading={loading} />;
}

View File

@@ -12,17 +12,19 @@ export default function JobPartsGraphAtom({
const data = useMemo(() => {
if (!job) return [];
const sums = job.joblines.reduce((acc, val) => {
if (!acc[val.part_type]) {
acc[val.part_type] = Dinero();
}
const sums = job.joblines
.filter((j) => !j.ignore)
.reduce((acc, val) => {
if (!acc[val.part_type]) {
acc[val.part_type] = Dinero();
}
acc[val.part_type] = acc[val.part_type].add(
Dinero({ amount: Math.round((val[price] || 0) * 100) })
);
acc[val.part_type] = acc[val.part_type].add(
Dinero({ amount: Math.round((val[price] || 0) * 100) })
);
return acc;
}, {});
return acc;
}, {});
return Object.keys(sums).map((key) => {
return {

View File

@@ -1,14 +1,16 @@
export default (part_type) => {
switch (part_type) {
case "PAA":
case "PAL":
case "PAC":
return "A/M";
case "PAE":
return "Exist.";
case "PAN":
case "PAP":
return "OEM";
case "PAL":
return "LKQ";
default:
return "?";
return part_type;
}
};

View File

@@ -0,0 +1,22 @@
import { Typography } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectDates } from "../../../redux/reporting/reporting.selectors";
import moment from "moment";
import { DateFormat } from "../../../util/constants";
const mapStateToProps = createStructuredSelector({
dates: selectDates,
});
export function ReportingTitleAtom({ dates }) {
return (
<Typography.Title level={2}>
{`RPS Report for Period from ${moment(dates.startDate).format(
DateFormat
)} to ${moment(dates.endDate).format(DateFormat)}`}
</Typography.Title>
);
}
export default connect(mapStateToProps, null)(ReportingTitleAtom);

View File

@@ -3,6 +3,7 @@ import { DatePicker, message, Spin } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { UPDATE_JOB } from "../../../graphql/jobs.queries";
import { DateFormat } from "../../../util/constants";
export default function CloseDateDisplayMolecule({ jobId, close_date }) {
const [editMode, setEditMode] = useState(false);
@@ -29,7 +30,7 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) {
return (
<div onBlur={() => setEditMode(false)}>
<DatePicker
value={value.isValid() ? value : null}
value={value && value.isValid() ? value : null}
onChange={handleChange}
/>
{loading && <Spin size="small" />}
@@ -38,7 +39,7 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) {
return (
<div style={{ cursor: "pointer" }} onClick={() => setEditMode(true)}>
{value.isValid() ? value.format("MM/DD/yyyy") : "No date set"}
{value && value.isValid() ? value.format(DateFormat) : "No date set"}
</div>
);
}

View File

@@ -3,6 +3,7 @@ import React from "react";
import CurrencyFormatterAtom from "../../atoms/currency-formatter/currency-formatter.atom";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import CloseDateDisplayMolecule from "../close-date-display/close-date-display.molecule";
import TimeAgoFormatter from "../../atoms/time-ago-formatter/time-ago-formatter.atom";
export default function JobsDetailDescriptionMolecule({ loading, job }) {
if (loading) return <Skeleton active />;
@@ -26,6 +27,9 @@ export default function JobsDetailDescriptionMolecule({ loading, job }) {
close_date={job.close_date}
/>
</Descriptions.Item>
<Descriptions.Item label="Last Updated">
<TimeAgoFormatter>{job.updated_at}</TimeAgoFormatter>
</Descriptions.Item>
</Descriptions>
</PageHeader>
</div>

View File

@@ -1,16 +1,19 @@
import { Table } from "antd";
import React from "react";
import { Input, Table } from "antd";
import React, { useState } from "react";
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";
export default function JobLinesTableMolecule({ loading, job }) {
const [searchText, setSearchText] = useState("");
const { joblines } = job;
const columns = [
{
title: "#",
dataIndex: "unq_seq",
key: "unq_seq",
dataIndex: "line_no",
key: "line_no",
},
{
title: "S#",
@@ -73,17 +76,47 @@ export default function JobLinesTableMolecule({ loading, job }) {
/>
),
},
{
title: "Ignore?",
dataIndex: "ignore",
key: "ignore",
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
],
onFilter: (value, record) => value === record.ignore,
render: (text, record) => (
<IgnoreJobLine lineId={record.id} ignore={record.ignore} />
),
},
];
const data =
searchText !== ""
? joblines.filter((j) =>
j.line_desc.toLowerCase().includes(searchText.toLowerCase())
)
: joblines;
return (
<div>
<Table
title={() => (
<Input.Search
placeholder="Search"
onSearch={(val) => {
setSearchText(val);
}}
enterButton
allowClear
/>
)}
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={false}
dataSource={joblines}
dataSource={data}
scroll={{
x: true,
y: "20rem",

View File

@@ -7,7 +7,7 @@ export default function JobsSearchFieldsMolecule({ callSearchQuery }) {
const handleFinish = (values) => {
callSearchQuery({
variables: {
search: values.search,
search: values.search || "",
startDate: (values.dateRange && values.dateRange[0]) || null,
endDate: (values.dateRange && values.dateRange[1]) || null,
},

View File

@@ -1,9 +1,12 @@
import { Skeleton, Statistic } from "antd";
import Dinero from "dinero.js";
import React, { useMemo } from "react";
import React, { useCallback } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectSelectedJobTargetPc } from "../../../redux/application/application.selectors";
import {
CalculateJobRpsDollars,
CalculateJobRpsPc,
} from "../../../util/CalculateJobRps";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
const mapStateToProps = createStructuredSelector({
@@ -22,31 +25,12 @@ export function JobsTargetsStatsMolecule({
job,
selectedJobTargetPc,
}) {
const currentRpsDollars = useMemo(() => {
if (!job) {
return 0;
}
return job.joblines.reduce((acc, val) => {
if (val.price_diff > 0) {
return acc.add(
Dinero({ amount: Math.round((val.price_diff || 0) * 100) })
);
} else {
return acc;
}
}, Dinero());
}, [job]);
const currentRpsDollars = useCallback(CalculateJobRpsDollars(job), [job]);
const currentRpsPc = useMemo(() => {
//TODO Redo this to do total of db price - act price / db price
if (!job) {
return 0;
}
const dbPriceSum = job.joblines.reduce((acc, val) => {
return acc + val.db_price;
}, 0);
return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1);
}, [job, currentRpsDollars]);
const currentRpsPc = useCallback(CalculateJobRpsPc(job, currentRpsDollars), [
job,
currentRpsDollars,
]);
if (loading) return <Skeleton active />;
if (!job) return <ErrorResultAtom title="Error displaying job data." />;
@@ -69,10 +53,9 @@ export function JobsTargetsStatsMolecule({
<Statistic
title="Current RPS %"
valueStyle={{
color:
selectedJobTargetPc * 100 > currentRpsPc ? "tomato" : "seagreen",
color: selectedJobTargetPc > currentRpsPc ? "tomato" : "seagreen",
}}
value={currentRpsPc}
value={(currentRpsPc * 100).toFixed(1)}
suffix="%"
/>
<Statistic title="Current RPS $" value={currentRpsDollars.toFormat()} />

View File

@@ -0,0 +1,44 @@
import { Button, DatePicker, Form } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { queryReportingData } from "../../../redux/reporting/reporting.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
queryReportingData: (dates) => dispatch(queryReportingData(dates)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReportingDatesMolecule);
export function ReportingDatesMolecule({ queryReportingData }) {
const [form] = Form.useForm();
const handleFinish = (values) => {
console.log("values", values);
queryReportingData({
startDate: values.dateRange[0],
endDate: values.dateRange[1],
});
};
return (
<Form form={form} onFinish={handleFinish}>
<div style={{ display: "flex" }}>
<Form.Item
label="Close Date Between"
name="dateRange"
rules={[{ type: "array", required: true }]}
>
<DatePicker.RangePicker />
</Form.Item>
<Button type="primary" htmlType="submit">
Run Search
</Button>
</div>
</Form>
);
}

View File

@@ -0,0 +1,147 @@
import { Input, Table } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectReportData,
selectReportLoading,
} from "../../../redux/reporting/reporting.selectors";
import { setSelectedJobId } from "../../../redux/application/application.actions";
import { Link } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
reportingLoading: selectReportLoading,
reportData: selectReportData,
});
const mapDispatchToProps = (dispatch) => ({
setSelectedJobId: (id) => dispatch(setSelectedJobId(id)),
});
export function ReportingJobsListMolecule({
reportingLoading,
reportData,
setSelectedJobId,
}) {
const [searchText, setSearchText] = useState("");
const columns = [
{
title: "Claim No.",
dataIndex: "clm_no",
key: "clm_no",
render: (text, record) => (
<Link onClick={() => setSelectedJobId(record.id)} to={"/"}>
{text}
</Link>
),
},
{
title: "Ins Co.",
dataIndex: "ins_co_nm",
key: "ins_co_nm",
},
{
title: "First Name",
dataIndex: "ownr_fn",
key: "ownr_fn",
},
{
title: "Last Name",
dataIndex: "ownr_ln",
key: "ownr_ln",
},
{
title: "Vehicle",
dataIndex: "vehicle",
key: "vehicle",
render: (text, record) =>
`${record.v_model_yr} ${record.v_makedesc} ${record.v_model} (${
record.v_type
}) - ${record.group} @ ${
record.v_age === 1 ? `${record.v_age} year` : `${record.v_age} years`
}`,
},
{
title: "Database Price Sum",
dataIndex: "dbPriceSum",
key: "dbPriceSum",
render: (text, record) => record.dbPriceSum.toFormat(),
},
{
title: "Actual Price Sum ",
dataIndex: "actPriceSum",
key: "actPriceSum",
render: (text, record) => record.actPriceSum.toFormat(),
},
{
title: "Price Diff.",
dataIndex: "jobRpsDollars",
key: "jobRpsDollars",
render: (text, record) => (
<span
style={{
color: record.jobRpsPc > record.jobTarget ? "seagreen" : "tomato",
}}
>
{`${record.jobRpsDollars.toFormat()} / ${record.expectedRpsDollars.toFormat()}`}
</span>
),
},
{
title: "Price Diff. %",
dataIndex: "price_diff_pc",
key: "price_diff_pc",
render: (text, record) => (
<span
style={{
color: record.jobRpsPc > record.jobTarget ? "seagreen" : "tomato",
}}
>
{`${(record.jobRpsPc * 100).toFixed(1)}% / ${(
record.jobTarget * 100
).toFixed(1)}%`}
</span>
),
},
];
const data =
searchText !== ""
? reportData.filter(
(j) =>
j.ownr_fn.toLowerCase().includes(searchText.toLowerCase()) ||
j.ownr_ln.toLowerCase().includes(searchText.toLowerCase()) ||
j.ownr_clm_no.toLowerCase().includes(searchText.toLowerCase())
)
: reportData;
return (
<div>
<Table
title={() => (
<Input.Search
placeholder="Search"
onSearch={(val) => {
setSearchText(val);
}}
enterButton
allowClear
/>
)}
columns={columns}
rowKey="id"
loading={reportingLoading}
size="small"
pagination={false}
dataSource={data}
scroll={{
x: true,
}}
/>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReportingJobsListMolecule);

View File

@@ -0,0 +1,72 @@
import { Skeleton, Statistic } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectReportLoading,
selectScorecard,
} from "../../../redux/reporting/reporting.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
const mapStateToProps = createStructuredSelector({
reportingLoading: selectReportLoading,
scoreCard: selectScorecard,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReportingTotalsStatsMolecule);
export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) {
if (reportingLoading) return <Skeleton active />;
if (!scoreCard)
return <ErrorResultAtom title="Error displaying score card data." />;
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-around",
marginTop: "1rem",
marginBottom: "1rem",
}}
>
<Statistic
title="RPS Total"
value={scoreCard.shopRpsTotalDollars.toFormat()}
/>
<Statistic
title="RPS Expectation"
value={scoreCard.shopRpsExpectedDollars.toFormat()}
/>
<Statistic
title="RPS Variance $"
valueStyle={{
color:
scoreCard.varianceDollars.getAmount() < 0 ? "tomato" : "seagreen",
}}
value={scoreCard.varianceDollars.toFormat()}
/>
<Statistic
title="Current RPS %"
valueStyle={{
color:
scoreCard.currentRpsPc <= scoreCard.targetRpsPc
? "tomato"
: "seagreen",
}}
value={(scoreCard.currentRpsPc * 100).toFixed(1)}
suffix="%"
/>
<Statistic
title="Target RPS %"
value={(scoreCard.targetRpsPc * 100).toFixed(1)}
suffix="%"
/>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { UserOutlined } from "@ant-design/icons";
import { LogoutOutlined } from "@ant-design/icons";
import { Menu } from "antd";
import React from "react";
import { connect } from "react-redux";
@@ -11,7 +11,7 @@ const mapDispatchToProps = (dispatch) => ({
export function SiderSignOut({ signOutStart, ...restProps }) {
return (
<Menu.Item
icon={<UserOutlined />}
icon={<LogoutOutlined />}
{...restProps}
onClick={() => signOutStart()}
>

View File

@@ -0,0 +1,54 @@
import { InputNumber, Switch } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import ipcTypes from "../../../ipc.types";
import { selectSettings } from "../../../redux/application/application.selectors";
import DataLabel from "../../atoms/data-label/data-label.atom";
const { ipcRenderer } = window;
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
appSettings: selectSettings,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function WatcherPollingMolecule({ appSettings }) {
const handlePollingToggle = (val) => {
ipcRenderer.send(ipcTypes.default.store.set, { "polling.enabled": val });
};
const handleIntervalChange = (val) => {
ipcRenderer.send(ipcTypes.default.store.set, {
"polling.pollingInterval": val,
});
};
return (
<div>
<DataLabel label="Polling Enabled? (Recommended for Network Monitoring)">
<Switch
onChange={handlePollingToggle}
checked={
appSettings && appSettings.polling && appSettings.polling.enabled
}
/>
</DataLabel>
<DataLabel label="Polling Interval">
<InputNumber
onChange={handleIntervalChange}
value={
appSettings &&
appSettings.polling &&
appSettings.polling.pollingInterval
}
/>
</DataLabel>
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(WatcherPollingMolecule);

View File

@@ -21,7 +21,6 @@ export function FilePathsList({ watchedPaths }) {
ipcRenderer.send(ipcTypes.default.fileWatcher.toMain.filepathsGet);
}, []);
console.log("watchedPaths", watchedPaths);
return (
<div>
<Typography.Title>Watcher File Paths</Typography.Title>

View File

@@ -2,6 +2,7 @@ import {
PieChartOutlined,
SettingFilled,
CloseOutlined,
BarChartOutlined,
} from "@ant-design/icons";
import { Menu } from "antd";
import React from "react";
@@ -19,6 +20,9 @@ export default function SiderMenuOrganism() {
<Menu.Item key="/" icon={<PieChartOutlined />}>
<Link to="/">Jobs</Link>
</Menu.Item>
<Menu.Item key="/reporting" icon={<BarChartOutlined />}>
<Link to="/reporting">Reporting</Link>
</Menu.Item>
<Menu.Item key="/settings" icon={<SettingFilled />}>
<Link to="/settings">Settings</Link>
</Menu.Item>

View File

@@ -1,15 +1,11 @@
import { Col, Row, Tabs, Grid } from "antd";
import { Col, Grid, Row, Tabs } from "antd";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import JobsDetailOrganism from "../../organisms/jobs-detail/jobs-detail.organism";
import JobsListOrganism from "../../organisms/jobs-list-latest/jobs-list-latest.organism";
import JobsListSearchOrganism from "../../organisms/jobs-list-search/jobs-list-search.organism";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({});
export function JobsPage() {
export default function JobsPage() {
console.log("Jobs Page Rerender");
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
@@ -47,4 +43,3 @@ export function JobsPage() {
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsPage);

View File

@@ -0,0 +1,31 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectDates } from "../../../redux/reporting/reporting.selectors";
import ReportingTitleAtom from "../../atoms/reporting-title/reporting-title.atom";
import ReportingDatesMolecule from "../../molecules/reporting-dates/reporting-dates.molecule";
import ReportingJobsListMolecule from "../../molecules/reporting-jobs-list/reporting-jobs-list.molecule";
import ReportingTotalsStatsMolecule from "../../molecules/reporting-totals-stats/reporting-totals-stats.molecule";
const mapStateToProps = createStructuredSelector({
dates: selectDates,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ReportingPage);
export function ReportingPage({ dates }) {
return (
<div>
<ReportingDatesMolecule />
{dates && dates.startDate && dates.endDate && (
<div>
<ReportingTitleAtom />
<ReportingTotalsStatsMolecule />
<ReportingJobsListMolecule />
</div>
)}
</div>
);
}

View File

@@ -1,13 +1,15 @@
import { Layout } from "antd";
import React from "react";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { Route } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
import Jobs from "../jobs/jobs.page";
import SettingsPage from "../settings/settings.page";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
import SiderMenuOrganism from "../../organisms/sider-menu/sider-menu.organism";
import JobsPage from "../jobs/jobs.page";
import ReportingPage from "../reporting/reporting.page";
import SettingsPage from "../settings/settings.page";
const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop });
const mapDispatchToProps = (dispatch) => ({});
@@ -19,6 +21,7 @@ export function RoutesPage({ bodyshop }) {
errorMessage="You do not currently have access to any shop. Please reach out to technical support."
/>
);
console.log("routes render");
return (
<Layout style={{ background: "#fff", height: "100vh" }} hasSider>
<Layout.Sider
@@ -30,10 +33,9 @@ export function RoutesPage({ bodyshop }) {
</Layout.Sider>
<Layout style={{ background: "#fff" }}>
<Layout.Content style={{ margin: "1rem", height: "100%" }}>
<Switch>
<Route exact path="/settings" component={SettingsPage} />
<Route path="/" component={Jobs} />
</Switch>
<Route exact path="/settings" component={SettingsPage} />
<Route exact path="/reporting" component={ReportingPage} />
<Route exact path="/" component={JobsPage} />
</Layout.Content>
</Layout>
</Layout>

View File

@@ -1,10 +1,17 @@
import { Col, Row } from "antd";
import React from "react";
import React, { useEffect } from "react";
import ipcTypes from "../../../ipc.types";
import WatcherPollingMolecule from "../../molecules/watcher-polling/watcher-polling.molecule";
import FilePathsListOrganism from "../../organisms/filepaths-list/filepaths-list.organism";
import ShopSettingsOrganism from "../../organisms/shop-settings/shop-settings.organism";
import WatcherManagerOrganism from "../../organisms/watcher-manager/watcher-manager.organism";
const { ipcRenderer } = window;
export default function SettingsPage() {
useEffect(() => {
ipcRenderer.send(ipcTypes.default.store.getAll);
}, []);
return (
<div>
<Row gutter={[16, 16]}>
@@ -13,6 +20,7 @@ export default function SettingsPage() {
</Col>
<Col span={6}>
<WatcherManagerOrganism />
<WatcherPollingMolecule />
</Col>
</Row>
@@ -20,11 +28,3 @@ export default function SettingsPage() {
</div>
);
}
// <Button
// onClick={() => {
// ipcRenderer.send(ipcTypes.default.filewatcher.start);
// }}
// >
// Start Watcher
// </Button>

View File

@@ -0,0 +1,23 @@
import gql from "graphql-tag";
export const UPDATE_JOB_LINE = gql`
mutation UPDATE_JOB_LINE($lineId: uuid!, $line: joblines_set_input!) {
update_joblines(where: { id: { _eq: $lineId } }, _set: $line) {
returning {
id
line_no
act_price
db_price
line_desc
line_ind
oem_partno
part_qty
part_type
unq_seq
price_diff
price_diff_pc
ignore
}
}
}
`;

View File

@@ -10,21 +10,6 @@ export const INSERT_NEW_JOB = gql`
}
`;
// on_conflict: {
// constraint: jobs_clm_no_bodyshopid_key
// update_columns: [
// ins_co_nm
// clm_no
// clm_total
// ownr_ln
// ownr_fn
// v_vin
// v_make_desc
// v_model_desc
// v_type
// ]
// }
export const QUERY_ALL_JOBS_PAGINATED = gql`
query QUERY_ALL_JOBS_PAGINATED(
$offset: Int
@@ -104,8 +89,10 @@ export const QUERY_JOB_BY_PK = gql`
v_age
loss_date
close_date
updated_at
joblines(order_by: { line_no: asc }) {
id
line_no
act_price
db_price
line_desc
@@ -116,6 +103,7 @@ export const QUERY_JOB_BY_PK = gql`
unq_seq
price_diff
price_diff_pc
ignore
}
}
}

View File

@@ -0,0 +1,49 @@
import gql from "graphql-tag";
export const REPORTING_GET_JOBS = gql`
query REPORTING_GET_JOBS($startDate: date, $endDate: date) {
jobs(
where: {
_and: [
{ close_date: { _gte: $startDate } }
{ close_date: { _lte: $endDate } }
{ close_date: { _is_null: false } }
]
}
) {
ownr_ln
ownr_fn
ins_co_nm
group
clm_total
clm_no
close_date
id
loss_date
updated_at
v_age
v_makedesc
v_model
v_model_yr
v_vin
v_type
joblines {
act_price
db_price
part_qty
part_type
price_diff
price_diff_pc
updated_at
oem_partno
line_no
line_ind
line_desc
ignore
id
db_ref
unq_seq
}
}
}
`;

View File

@@ -2,7 +2,7 @@ import "antd/dist/antd.css";
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { MemoryRouter } from "react-router-dom";
import { PersistGate } from "redux-persist/integration/react";
import App from "./App/App";
import "./index.css";
@@ -11,11 +11,11 @@ require("dotenv").config();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<MemoryRouter>
<PersistGate persistor={persistor}>
<App />
</PersistGate>
</BrowserRouter>
</MemoryRouter>
</Provider>,
document.getElementById("root")
);

View File

@@ -9,6 +9,12 @@ exports.default = {
setAcceptableInsCoNm: "setAcceptableInsCoNm",
},
},
store: {
get: "store__get",
getAll: "store_getAll",
set: "store_set",
response: "store_response",
},
fileWatcher: {
toMain: {
filepathsGet: "filewatcher__filepathsget",
@@ -16,6 +22,7 @@ exports.default = {
stop: "filewatcher__stop",
addPath: "filewatcher__addPath",
removePath: "filewatcher__removePath",
setPolling: "filewatcher__setPolling",
},
toRenderer: {
filepathsList: "filewatcher__filepathslist",
@@ -23,6 +30,7 @@ exports.default = {
startFailure: "filewatcher__start-failure",
stopSuccess: "filewatcher__stop-success",
error: "filewatcher__error",
getPolling: "filewatcher__getPolling",
},
},
estimate: {

View File

@@ -1,5 +1,6 @@
import ipcTypes from "../ipc.types";
import {
setSettings,
setWatchedPaths,
setWatcherStatus,
} from "../redux/application/application.actions";
@@ -52,3 +53,7 @@ ipcRenderer.on(
await UpsertEstimate(obj);
}
);
ipcRenderer.on(ipcTypes.default.store.response, (event, obj) => {
store.dispatch(setSettings(obj));
});

View File

@@ -38,3 +38,7 @@ export const setSelectedJobTargetPcSuccess = (pct) => ({
type: ApplicationActionTypes.SET_SELECTED_JOB_TARGET_PC_SUCCESS,
payload: pct,
});
export const setSettings = (settingsObj) => ({
type: ApplicationActionTypes.SET_SETTINGS,
payload: settingsObj,
});

View File

@@ -5,6 +5,7 @@ const INITIAL_STATE = {
watcherError: null,
selectedJobId: null,
selectedJobTargetPc: 100,
settings: {},
};
const applicationReducer = (state = INITIAL_STATE, action) => {
@@ -41,6 +42,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
};
case ApplicationActionTypes.SET_SELECTED_JOB_ID:
return { ...state, selectedJobId: action.payload };
case ApplicationActionTypes.SET_SETTINGS:
return { ...state, settings: { ...state.settings, ...action.payload } };
default:
return state;
}

View File

@@ -1,4 +1,5 @@
import { all, call, takeLatest, select, put } from "redux-saga/effects";
import GetJobTarget from "../../util/GetJobTarget";
import { setSelectedJobTargetPcSuccess } from "./application.actions";
import ApplicationActionTypes from "./application.types";
@@ -12,17 +13,18 @@ export function* CalculateTarget({ payload }) {
const { group, v_age } = payload;
const targets = yield select((state) => state.user.bodyshop.targets);
const targetsForGroup = targets.filter((t) => t.group === group);
if (!targetsForGroup) return 0;
const targetPc = targetsForGroup.filter(
(t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true)
);
if (targetPc.length === 0) yield put(setSelectedJobTargetPcSuccess(100));
else if (targetPc.length === 1)
yield put(setSelectedJobTargetPcSuccess(targetPc[0].target));
else {
yield put(setSelectedJobTargetPcSuccess(100));
}
yield put(setSelectedJobTargetPcSuccess(GetJobTarget(group, v_age, targets)));
// const targetsForGroup = targets.filter((t) => t.group === group);
// if (!targetsForGroup) return 0;
// const targetPc = targetsForGroup.filter(
// (t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true)
// );
// if (targetPc.length === 0) yield put(setSelectedJobTargetPcSuccess(100));
// else if (targetPc.length === 1)
// yield put(setSelectedJobTargetPcSuccess(targetPc[0].target));
// else {
// yield put(setSelectedJobTargetPcSuccess(100));
// }
}
export function* applicationSagas() {

View File

@@ -26,3 +26,8 @@ export const selectSelectedJobTargetPc = createSelector(
[selectApplication],
(application) => application.selectedJobTargetPc
);
export const selectSettings = createSelector(
[selectApplication],
(application) => application.settings
);

View File

@@ -7,5 +7,6 @@ const ApplicationActionTypes = {
SET_SELECTED_JOB_ID: "SET_SELECTED_JOB_ID",
SET_SELECTED_JOB_TARGET_PC: "SET_SELECTED_JOB_TARGET_PC",
SET_SELECTED_JOB_TARGET_PC_SUCCESS: "SET_SELECTED_JOB_TARGET_PC_SUCCESS",
SET_SETTINGS: "SET_SETTINGS",
};
export default ApplicationActionTypes;

View File

@@ -0,0 +1,24 @@
import ReportingActionTypes from "./reporting.types";
export const queryReportingData = ({ startDate, endDate }) => ({
type: ReportingActionTypes.QUERY_REPORTING_DATA,
payload: { startDate, endDate },
});
export const setReportingData = (data) => ({
type: ReportingActionTypes.SET_REPORTING_DATA,
payload: data,
});
export const calculateScorecard = (data) => ({
type: ReportingActionTypes.CALCULATE_SCORE_CARD,
payload: data,
});
export const setScoreCard = (data) => ({
type: ReportingActionTypes.SET_SCORE_CARD,
payload: data,
});
export const setReportingError = (data) => ({
type: ReportingActionTypes.SET_REPORTING_ERROR,
payload: data,
});

View File

@@ -0,0 +1,30 @@
import ReportingActionTypes from "./reporting.types";
const INITIAL_STATE = {
dates: { startDate: null, endDate: null },
data: [],
scoreCard: null,
error: null,
loading: false,
};
const applicationReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ReportingActionTypes.QUERY_REPORTING_DATA:
return {
...state,
loading: true,
dates: {
startDate: action.payload.startDate.toISOString(),
endDate: action.payload.endDate.toISOString(),
},
};
case ReportingActionTypes.SET_REPORTING_DATA:
return { ...state, data: action.payload };
case ReportingActionTypes.SET_SCORE_CARD:
return { ...state, loading: false, scoreCard: action.payload };
default:
return state;
}
};
export default applicationReducer;

View File

@@ -0,0 +1,128 @@
import { all, call, takeLatest, select, put } from "redux-saga/effects";
import {
calculateScorecard,
setReportingData,
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;
export function* onQueryReportData() {
yield takeLatest(
ReportingApplicationTypes.QUERY_REPORTING_DATA,
queryReportingData
);
}
export function* queryReportingData({ payload: { startDate, endDate } }) {
const result = yield client.query({
query: REPORTING_GET_JOBS,
variables: { startDate, endDate },
});
if (result.errors) {
log.error("Error fetching report data.", result.errors);
yield put(setReportingData(null));
} else {
yield put(calculateScorecard(result.data.jobs));
}
}
export function* onSetReportData() {
yield takeLatest(
ReportingApplicationTypes.SET_REPORTING_DATA,
handleSetReportData
);
}
export function* handleSetReportData({ payload: jobs }) {
// yield put(calculateScorecard(jobs));
}
export function* onCalculateScoreCard() {
yield takeLatest(
ReportingApplicationTypes.CALCULATE_SCORE_CARD,
handleCalculateScoreCard
);
}
export function* handleCalculateScoreCard({ payload: jobs }) {
console.log("jobs", jobs);
const targets = yield select((state) => state.user.bodyshop.targets);
const scoreCard = {
shopRpsTotalDollars: Dinero(),
shopRpsExpectedDollars: Dinero(),
varianceDollars: null,
variancePc: 0,
allJobsSumDbPrice: Dinero(),
allJobsSumActPrice: Dinero(),
currentRpsPc: 0,
targetRpsPc: 0,
};
//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,
};
});
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));
}
export function* reportingSagas() {
yield all([
call(onQueryReportData),
call(onSetReportData),
call(onCalculateScoreCard),
]);
}

View File

@@ -0,0 +1,49 @@
import { createSelector } from "reselect";
const selectReporting = (state) => state.reporting;
export const selectReportLoading = createSelector(
[selectReporting],
(reporting) => reporting.loading
);
export const selectDates = createSelector(
[selectReporting],
(reporting) => reporting.dates
);
export const selectScorecard = createSelector(
[selectReporting],
(reporting) => reporting.scoreCard
);
export const selectReportingError = createSelector(
[selectReporting],
(reporting) => reporting.error
);
export const selectReportData = createSelector(
[selectReporting],
(reporting) => reporting.data
);
// export const selectWatchedPaths = createSelector(
// [selectReporting],
// (application) => application.watchedPaths
// );
// export const selectWatcherError = createSelector(
// [selectReporting],
// (application) => application.watcherError
// );
// export const selectSelectedJobId = createSelector(
// [selectReporting],
// (application) => application.selectedJobId
// );
// export const selectSelectedJobTargetPc = createSelector(
// [selectReporting],
// (application) => application.selectedJobTargetPc
// );
// export const selectSettings = createSelector(
// [selectReporting],
// (application) => application.settings
// );

View File

@@ -0,0 +1,8 @@
const ReportingActionTypes = {
QUERY_REPORTING_DATA: "QUERY_REPORTING_DATA",
CALCULATE_SCORE_CARD: "CALCULATE_SCORE_CARD",
SET_REPORTING_DATA: "SET_REPORTING_DATA",
SET_SCORE_CARD: "SET_SCORE_CARD",
SET_REPORTING_ERROR: "SET_REPORTING_ERROR",
};
export default ReportingActionTypes;

View File

@@ -3,16 +3,18 @@ import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
import applicationReducer from "./application/application.reducer";
import userReducer from "./user/user.reducer";
import reportingReducer from "./reporting/reporting.reducer";
const persistConfig = {
key: "root",
storage,
blacklist: ["application", "user"],
blacklist: ["application", "user", "reporting"],
};
const rootReducer = combineReducers({
application: applicationReducer,
user: userReducer,
reporting: reportingReducer,
});
export default persistReducer(persistConfig, rootReducer);

View File

@@ -1,7 +1,7 @@
import { all, call } from "redux-saga/effects";
import { applicationSagas } from "./application/application.sagas";
import { userSagas } from "./user/user.sagas";
import { reportingSagas } from "./reporting/reporting.sagas";
export default function* rootSaga() {
yield all([call(applicationSagas), call(userSagas)]);
yield all([call(applicationSagas), call(userSagas), call(reportingSagas)]);
}

View File

@@ -0,0 +1,42 @@
import Dinero from "dinero.js";
export function CalculateJobRpsDollars(job, returnSumActPrice) {
if (!job) {
return 0;
}
let actPriceSum = Dinero();
const jobRpsDollars = job.joblines
.filter((j) => !j.ignore)
.reduce((acc, val) => {
actPriceSum = actPriceSum.add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) })
);
if (val.price_diff > 0) {
return acc.add(
Dinero({ amount: Math.round((val.price_diff || 0) * 100) })
);
} else {
return acc;
}
}, Dinero());
return returnSumActPrice ? { actPriceSum, jobRpsDollars } : jobRpsDollars;
}
export function CalculateJobRpsPc(
job,
currentRpsDollars,
returnSumDbPrice = false
) {
//TODO Redo this to do total of db price - act price / db price
if (!job) {
return 0;
}
const dbPriceSum = job.joblines
.filter((j) => !j.ignore)
.reduce((acc, val) => {
return acc.add(Dinero({ amount: Math.round((val.db_price || 0) * 100) }));
}, Dinero());
const jobRpsPc = currentRpsDollars.getAmount() / dbPriceSum.getAmount();
return returnSumDbPrice ? { dbPriceSum, jobRpsPc } : jobRpsPc;
}

12
src/util/GetJobTarget.js Normal file
View File

@@ -0,0 +1,12 @@
export default function GetJobTarget(group, v_age, targets) {
const targetsForGroup = targets.filter((t) => t.group === group);
if (!targetsForGroup) return 0;
const targetPc = targetsForGroup.filter(
(t) => t.ageGte <= v_age && (t.ageLt ? t.ageLt > v_age : true)
);
if (targetPc.length === 0) return 1;
else if (targetPc.length === 1) return targetPc[0].target;
else {
return 1;
}
}

1
src/util/constants.js Normal file
View File

@@ -0,0 +1 @@
export const DateFormat = "MM/DD/yyyy";