From 329c9750198709a9d73ded2bdfd9a084fa645b20 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 20 Oct 2020 16:37:39 -0700 Subject: [PATCH] WIP report. Added stats, sagas for calculation, formatting. --- .../close-date-display.molecule.jsx | 4 +- .../jobs-search-fields.molecule.jsx | 2 +- .../jobs-targets-stats.molecule.jsx | 5 +- .../reporting-jobs-list.molecule.jsx | 140 ++++++++++++++++++ .../reporting-totals-stats.molecule.jsx | 73 +++++++++ .../pages/reporting/reporting.page.jsx | 21 ++- src/graphql/reporting.queries.js | 2 + src/redux/reporting/reporting.sagas.js | 81 +++++++++- src/util/CalculateJobRps.js | 23 ++- src/util/GetJobTarget.js | 4 +- 10 files changed, 335 insertions(+), 20 deletions(-) create mode 100644 src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx create mode 100644 src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx diff --git a/src/components/molecules/close-date-display/close-date-display.molecule.jsx b/src/components/molecules/close-date-display/close-date-display.molecule.jsx index d9a29ed..1f54701 100644 --- a/src/components/molecules/close-date-display/close-date-display.molecule.jsx +++ b/src/components/molecules/close-date-display/close-date-display.molecule.jsx @@ -29,7 +29,7 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) { return (
setEditMode(false)}> {loading && } @@ -38,7 +38,7 @@ export default function CloseDateDisplayMolecule({ jobId, close_date }) { return (
setEditMode(true)}> - {value.isValid() ? value.format("MM/DD/yyyy") : "No date set"} + {value && value.isValid() ? value.format("MM/DD/yyyy") : "No date set"}
); } diff --git a/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx b/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx index 45765d0..bd03303 100644 --- a/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx +++ b/src/components/molecules/jobs-search-fields/jobs-search-fields.molecule.jsx @@ -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, }, diff --git a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx index 2c7008e..f7a7ccc 100644 --- a/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx +++ b/src/components/molecules/jobs-targets-stats/jobs-targets-stats.molecule.jsx @@ -53,10 +53,9 @@ export function JobsTargetsStatsMolecule({ currentRpsPc ? "tomato" : "seagreen", + color: selectedJobTargetPc > currentRpsPc ? "tomato" : "seagreen", }} - value={currentRpsPc} + value={(currentRpsPc * 100).toFixed(1)} suffix="%" /> diff --git a/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx new file mode 100644 index 0000000..829ed73 --- /dev/null +++ b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx @@ -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) => ( + record.jobTarget ? "seagreen" : "tomato", + }} + > + {`${record.jobRpsDollars.toFormat()} / ${record.expectedRpsDollars.toFormat()}`} + + ), + }, + { + title: "Price Diff. %", + dataIndex: "price_diff_pc", + key: "price_diff_pc", + render: (text, record) => ( + record.jobTarget ? "seagreen" : "tomato", + }} + > + {`${(record.jobRpsPc * 100).toFixed(1)}% / ${( + record.jobTarget * 100 + ).toFixed(1)}%`} + + ), + }, + ]; + + 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 ( +
+ ( + { + setSearchText(val); + }} + enterButton + allowClear + /> + )} + columns={columns} + rowKey="id" + loading={reportingLoading} + size="small" + pagination={false} + dataSource={data} + scroll={{ + x: true, + }} + /> + + ); +} diff --git a/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx new file mode 100644 index 0000000..63adb2c --- /dev/null +++ b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx @@ -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 ; + if (!scoreCard) + return ; + + return ( +
+ + + + + +
+ ); +} diff --git a/src/components/pages/reporting/reporting.page.jsx b/src/components/pages/reporting/reporting.page.jsx index 73a57d9..f6e6949 100644 --- a/src/components/pages/reporting/reporting.page.jsx +++ b/src/components/pages/reporting/reporting.page.jsx @@ -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 (
+ {dates && dates.startDate && dates.endDate && ( +
+ + +
+ )}
); } diff --git a/src/graphql/reporting.queries.js b/src/graphql/reporting.queries.js index a7bbfb5..9625ca4 100644 --- a/src/graphql/reporting.queries.js +++ b/src/graphql/reporting.queries.js @@ -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 diff --git a/src/redux/reporting/reporting.sagas.js b/src/redux/reporting/reporting.sagas.js index 5d02944..5624d96 100644 --- a/src/redux/reporting/reporting.sagas.js +++ b/src/redux/reporting/reporting.sagas.js @@ -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() { diff --git a/src/util/CalculateJobRps.js b/src/util/CalculateJobRps.js index f31aa09..c97aeda 100644 --- a/src/util/CalculateJobRps.js +++ b/src/util/CalculateJobRps.js @@ -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; } diff --git a/src/util/GetJobTarget.js b/src/util/GetJobTarget.js index 4919eaa..df93f1a 100644 --- a/src/util/GetJobTarget.js +++ b/src/util/GetJobTarget.js @@ -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; } }