WIP report. Added stats, sagas for calculation, formatting.

This commit is contained in:
Patrick Fic
2020-10-20 16:37:39 -07:00
parent 045346ce48
commit 329c975019
10 changed files with 335 additions and 20 deletions

View File

@@ -29,7 +29,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 +38,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("MM/DD/yyyy") : "No date set"}
</div>
);
}

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

@@ -53,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,140 @@
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";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectReportData,
selectReportLoading,
} from "../../../redux/reporting/reporting.selectors";
const mapStateToProps = createStructuredSelector({
reportingLoading: selectReportLoading,
reportData: selectReportData,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ReportingJobsListMolecule);
export function ReportingJobsListMolecule({ reportingLoading, reportData }) {
const [searchText, setSearchText] = useState("");
const columns = [
{
title: "Claim No.",
dataIndex: "clm_no",
key: "clm_no",
},
{
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>
);
}

View File

@@ -0,0 +1,73 @@
import { Skeleton, Statistic } from "antd";
import React, { useCallback } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectSelectedJobTargetPc } from "../../../redux/application/application.selectors";
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,10 +1,29 @@
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectDates } from "../../../redux/reporting/reporting.selectors";
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";
export default function ReportingPage() {
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>
<ReportingTotalsStatsMolecule />
<ReportingJobsListMolecule />
</div>
)}
</div>
);
}

View File

@@ -25,6 +25,8 @@ export const REPORTING_GET_JOBS = gql`
v_makedesc
v_model
v_model_yr
v_vin
v_type
joblines {
act_price
db_price

View File

@@ -1,8 +1,19 @@
import { all, call, takeLatest, select, put } from "redux-saga/effects";
import { calculateScorecard, setReportingData } from "./reporting.actions";
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() {
@@ -20,7 +31,7 @@ export function* queryReportingData({ payload: { startDate, endDate } }) {
log.error("Error fetching report data.", result.errors);
yield put(setReportingData(null));
} else {
yield put(setReportingData(result.data.jobs));
yield put(calculateScorecard(result.data.jobs));
}
}
@@ -31,7 +42,7 @@ export function* onSetReportData() {
);
}
export function* handleSetReportData({ payload: jobs }) {
yield put(calculateScorecard(jobs));
// yield put(calculateScorecard(jobs));
}
export function* onCalculateScoreCard() {
@@ -42,10 +53,70 @@ export function* onCalculateScoreCard() {
}
export function* handleCalculateScoreCard({ payload: jobs }) {
console.log("jobs", jobs);
// yield put(calculateScorecard(jobs));
const targets = yield select((state) => state.user.bodyshop.targets);
//Get the RPS on a per job basis.
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() {

View File

@@ -1,12 +1,16 @@
import Dinero from "dinero.js";
export function CalculateJobRpsDollars(job) {
export function CalculateJobRpsDollars(job, returnSumActPrice) {
if (!job) {
return 0;
}
return job.joblines
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) })
@@ -15,9 +19,14 @@ export function CalculateJobRpsDollars(job) {
return acc;
}
}, Dinero());
return returnSumActPrice ? { actPriceSum, jobRpsDollars } : jobRpsDollars;
}
export function CalculateJobRpsPc(job, currentRpsDollars) {
export function CalculateJobRpsPc(
job,
currentRpsDollars,
returnSumDbPrice = false
) {
//TODO Redo this to do total of db price - act price / db price
if (!job) {
return 0;
@@ -25,7 +34,9 @@ export function CalculateJobRpsPc(job, currentRpsDollars) {
const dbPriceSum = job.joblines
.filter((j) => !j.ignore)
.reduce((acc, val) => {
return acc + val.db_price;
}, 0);
return (currentRpsDollars.getAmount() / dbPriceSum).toFixed(1);
return acc.add(Dinero({ amount: Math.round((val.db_price || 0) * 100) }));
}, Dinero());
const jobRpsPc = currentRpsDollars.getAmount() / dbPriceSum.getAmount();
return returnSumDbPrice ? { dbPriceSum, jobRpsPc } : jobRpsPc;
}

View File

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