Initial scenario manager for reporting.
This commit is contained in:
@@ -1,31 +1,48 @@
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { Alert, Badge, Input, Space, Table, Tooltip } from "antd";
|
||||
import { Alert, Badge, Input, Space, Table, Tooltip, Switch } from "antd";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import ipcTypes from "../../../ipc.types";
|
||||
import { setSelectedJobId } from "../../../redux/application/application.actions";
|
||||
import { selectReportData, selectReportLoading, selectScorecard } from "../../../redux/reporting/reporting.selectors";
|
||||
import {
|
||||
selectExcludedIds,
|
||||
selectReportData,
|
||||
selectReportLoading,
|
||||
selectScorecard
|
||||
} from "../../../redux/reporting/reporting.selectors";
|
||||
import dayjs from "../../../util/day.js";
|
||||
import { alphaSort } from "../../../util/sorters";
|
||||
import RequiresReimportDisplay from "../../atoms/requires-reimport/requires-reimport.atom.jsx";
|
||||
import VehicleGroupAlertAtom from "../../atoms/vehicle-group-alert/vehicle-group-alert.atom";
|
||||
import GroupVerifySwitch from "../group-verify-switch/group-verify-switch.component";
|
||||
import JobsClaimsClerkMolecule from "../jobs-claims-clerk/jobs-claims-clerk.molecule.jsx";
|
||||
import { addExcludedId, removeExcludedId } from "../../../redux/reporting/reporting.actions.js";
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
excludedIds: selectExcludedIds,
|
||||
reportingLoading: selectReportLoading,
|
||||
reportData: selectReportData,
|
||||
scoreCard: selectScorecard
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSelectedJobId: (id) => dispatch(setSelectedJobId(id))
|
||||
setSelectedJobId: (id) => dispatch(setSelectedJobId(id)),
|
||||
addExcludedId: (id) => dispatch(addExcludedId(id)),
|
||||
removeExcludedId: (id) => dispatch(removeExcludedId(id))
|
||||
});
|
||||
|
||||
export function ReportingJobsListMolecule({ scoreCard, reportingLoading, reportData, setSelectedJobId }) {
|
||||
export function ReportingJobsListMolecule({
|
||||
scoreCard,
|
||||
reportingLoading,
|
||||
reportData,
|
||||
setSelectedJobId,
|
||||
excludedIds,
|
||||
addExcludedId,
|
||||
removeExcludedId
|
||||
}) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const columns = [
|
||||
@@ -40,7 +57,7 @@ export function ReportingJobsListMolecule({ scoreCard, reportingLoading, reportD
|
||||
|
||||
{record.alerts && record.alerts.length > 0 && (
|
||||
<Tooltip title="Claims Clerk AI has detected possible issues with this estimate. Review the estimate to ensure you are following MPI guidelines.">
|
||||
<Badge count={record.alerts.length} size="small" />
|
||||
<Badge count={record.alerts.length} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<RequiresReimportDisplay job={record} />
|
||||
@@ -134,6 +151,20 @@ export function ReportingJobsListMolecule({ scoreCard, reportingLoading, reportD
|
||||
{`${(record.jobRpsPc * 100 || 0).toFixed(1)}% / ${(record.jobTarget * 100).toFixed(1)}%`}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Temporarily Exclude",
|
||||
dataIndex: "scenario_manager",
|
||||
key: "scenario_manager",
|
||||
render: (text, record) => (
|
||||
<Switch
|
||||
checked={excludedIds.includes(record.id)}
|
||||
onChange={(checked) => {
|
||||
console.log("Chcekedd", checked);
|
||||
checked ? addExcludedId(record.id) : removeExcludedId(record.id);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,90 +1,95 @@
|
||||
import { Skeleton, Statistic } from "antd";
|
||||
import { Alert, Button, 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 { selectExcludedIds, selectReportLoading, selectScorecard } from "../../../redux/reporting/reporting.selectors";
|
||||
import ErrorResultAtom from "../../atoms/error-result/error-result.atom";
|
||||
import { clearExcludedIds } from "../../../redux/reporting/reporting.actions";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportingLoading: selectReportLoading,
|
||||
scoreCard: selectScorecard,
|
||||
excludedJobIds: selectExcludedIds
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
clearExcludedIds: () => dispatch(clearExcludedIds())
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ReportingTotalsStatsMolecule);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ReportingTotalsStatsMolecule);
|
||||
|
||||
export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard }) {
|
||||
export function ReportingTotalsStatsMolecule({ reportingLoading, scoreCard, excludedJobIds, clearExcludedIds }) {
|
||||
if (reportingLoading) return <Skeleton active />;
|
||||
if (!scoreCard)
|
||||
return <ErrorResultAtom title="Error displaying score card data." />;
|
||||
if (!scoreCard) return <ErrorResultAtom title="Error displaying score card data." />;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-around",
|
||||
marginTop: "1rem",
|
||||
marginBottom: "1rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Statistic
|
||||
title="Target RPS %"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
value={((scoreCard.targetRpsPc || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
<Statistic
|
||||
title="Current RPS %"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
valueStyle={{
|
||||
color:
|
||||
(scoreCard.currentRpsPc || 0) <= (scoreCard.targetRpsPc || 0)
|
||||
? "tomato"
|
||||
: "seagreen",
|
||||
}}
|
||||
value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
<Statistic
|
||||
title="RPS Variance %"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
valueStyle={{
|
||||
color: scoreCard.variancePc < 0 ? "tomato" : "seagreen",
|
||||
}}
|
||||
value={((scoreCard.variancePc || 0) * 100).toFixed(2)}
|
||||
suffix="%"
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-around",
|
||||
marginTop: "1rem",
|
||||
marginBottom: "1rem"
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Statistic
|
||||
title="Target RPS %"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
value={((scoreCard.targetRpsPc || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
<Statistic
|
||||
title="Current RPS %"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
valueStyle={{
|
||||
color: (scoreCard.currentRpsPc || 0) <= (scoreCard.targetRpsPc || 0) ? "tomato" : "seagreen"
|
||||
}}
|
||||
value={((scoreCard.currentRpsPc || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
<Statistic
|
||||
title="RPS Variance %"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
valueStyle={{
|
||||
color: scoreCard.variancePc < 0 ? "tomato" : "seagreen"
|
||||
}}
|
||||
value={((scoreCard.variancePc || 0) * 100).toFixed(2)}
|
||||
suffix="%"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Statistic
|
||||
title="Target RPS $"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
value={scoreCard.shopRpsExpectedDollars.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title="Current RPS $"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
value={scoreCard.shopRpsTotalDollars.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title="RPS Variance $"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
valueStyle={{
|
||||
color: scoreCard.varianceDollars.getAmount() < 0 ? "tomato" : "seagreen"
|
||||
}}
|
||||
value={scoreCard.varianceDollars.toFormat()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Statistic
|
||||
title="Target RPS $"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
value={scoreCard.shopRpsExpectedDollars.toFormat()}
|
||||
{excludedJobIds.length > 0 && (
|
||||
<Alert
|
||||
type="warning"
|
||||
action={
|
||||
<Button size="small" danger onClick={() => clearExcludedIds()}>
|
||||
Clear
|
||||
</Button>
|
||||
}
|
||||
message={`There are ${excludedJobIds.length} jobs excluded from the totals above.`}
|
||||
/>
|
||||
<Statistic
|
||||
title="Current RPS $"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
value={scoreCard.shopRpsTotalDollars.toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title="RPS Variance $"
|
||||
style={{ margin: "0rem .5rem" }}
|
||||
valueStyle={{
|
||||
color:
|
||||
scoreCard.varianceDollars.getAmount() < 0 ? "tomato" : "seagreen",
|
||||
}}
|
||||
value={scoreCard.varianceDollars.toFormat()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,5 +34,21 @@ export const setAuditResults = (auditResults) => ({
|
||||
type: ReportingActionTypes.SET_AUDIT_RESULTS,
|
||||
payload: auditResults
|
||||
});
|
||||
export const addExcludedId = (id) => ({
|
||||
type: ReportingActionTypes.ADD_EXCLUDED_ID,
|
||||
payload: id
|
||||
});
|
||||
export const removeExcludedId = (id) => ({
|
||||
type: ReportingActionTypes.REMOVE_EXCLUDED_ID,
|
||||
payload: id
|
||||
});
|
||||
export const clearExcludedIds = () => ({
|
||||
type: ReportingActionTypes.CLEAR_EXCLUDED_IDS
|
||||
});
|
||||
|
||||
export const setCachedJobs = (jobs) => ({
|
||||
type: ReportingActionTypes.SET_CACHED_JOBS,
|
||||
payload: jobs
|
||||
});
|
||||
|
||||
export const setAuditError = (error) => ({ type: ReportingActionTypes.SET_AUDIT_ERROR, payload: error });
|
||||
|
||||
@@ -7,7 +7,9 @@ const INITIAL_STATE = {
|
||||
loading: false,
|
||||
auditLoading: false,
|
||||
audit: {},
|
||||
auditError: null
|
||||
auditError: null,
|
||||
excludedIds: [],
|
||||
cachedJobs: []
|
||||
};
|
||||
|
||||
const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
@@ -37,6 +39,14 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
return { ...state, loading: false, auditLoading: false, auditError: null, audit: action.payload };
|
||||
case ReportingActionTypes.SET_AUDIT_ERROR:
|
||||
return { ...state, auditLoading: false, auditError: action.payload };
|
||||
case ReportingActionTypes.ADD_EXCLUDED_ID:
|
||||
return { ...state, excludedIds: [...state.excludedIds, action.payload] };
|
||||
case ReportingActionTypes.REMOVE_EXCLUDED_ID:
|
||||
return { ...state, excludedIds: state.excludedIds.filter((id) => id !== action.payload) };
|
||||
case ReportingActionTypes.CLEAR_EXCLUDED_IDS:
|
||||
return { ...state, excludedIds: [] };
|
||||
case ReportingActionTypes.SET_CACHED_JOBS:
|
||||
return { ...state, cachedJobs: action.payload };
|
||||
case ReportingActionTypes.TOGGLE_GROUP_VERIFIED:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
setScoreCard,
|
||||
setReportingError,
|
||||
setAuditResults,
|
||||
setAuditError
|
||||
setAuditError,
|
||||
setCachedJobs
|
||||
} from "./reporting.actions";
|
||||
import ReportingApplicationTypes from "./reporting.types";
|
||||
|
||||
@@ -21,6 +22,15 @@ const { log, ipcRenderer } = window;
|
||||
export function* onQueryReportData() {
|
||||
yield takeLatest(ReportingApplicationTypes.QUERY_REPORTING_DATA, queryReportingData);
|
||||
}
|
||||
export function* onAddExcludedIds() {
|
||||
yield takeLatest(ReportingApplicationTypes.ADD_EXCLUDED_ID, handleCalculateScoreCard);
|
||||
}
|
||||
export function* onRemoveExcludedId() {
|
||||
yield takeLatest(ReportingApplicationTypes.REMOVE_EXCLUDED_ID, handleCalculateScoreCard);
|
||||
}
|
||||
export function* onClearExcludedIds() {
|
||||
yield takeLatest(ReportingApplicationTypes.CLEAR_EXCLUDED_IDS, handleCalculateScoreCard);
|
||||
}
|
||||
export function* queryReportingData({ payload: { startDate, endDate } }) {
|
||||
const result = yield client.query({
|
||||
query: REPORTING_GET_JOBS,
|
||||
@@ -33,6 +43,7 @@ export function* queryReportingData({ payload: { startDate, endDate } }) {
|
||||
log.error("Error fetching report data.", result.errors);
|
||||
yield put(setReportingData(null));
|
||||
} else {
|
||||
yield put(setCachedJobs(result.data.jobs));
|
||||
yield put(calculateScorecard(result.data.jobs));
|
||||
}
|
||||
}
|
||||
@@ -90,15 +101,16 @@ export function* handleCalculateAudit({ payload: claimsArrayFromAudit }) {
|
||||
export function* onCalculateScoreCard() {
|
||||
yield takeLatest(ReportingApplicationTypes.CALCULATE_SCORE_CARD, handleCalculateScoreCard);
|
||||
}
|
||||
export function* handleCalculateScoreCard({ payload: jobs }) {
|
||||
export function* handleCalculateScoreCard({ payload: queriedJobs }) {
|
||||
try {
|
||||
ipcRenderer.send(ipcTypes.app.toMain.track, {
|
||||
event: "CALCULATE_SCORECARD"
|
||||
});
|
||||
|
||||
const targets = yield select((state) => state.user.bodyshop.targets);
|
||||
//const groups = yield select((state) => state.user.bodyshop.groups);
|
||||
|
||||
const excludedJobIds = yield select((state) => state.reporting.excludedIds);
|
||||
const cachedJobs = yield select((state) => state.reporting.cachedJobs);
|
||||
let jobs = Array.isArray(queriedJobs) ? queriedJobs : cachedJobs;
|
||||
//Check to ensure every job has a group.
|
||||
const jobsWithNoGroup = jobs
|
||||
.filter((j) => !j.group)
|
||||
@@ -135,6 +147,7 @@ export function* handleCalculateScoreCard({ payload: jobs }) {
|
||||
|
||||
//Get the RPS on a per job basis.
|
||||
jobs = jobs.map((job) => {
|
||||
const isJobExcludedFromCalculation = excludedJobIds.includes(job.id);
|
||||
const { actPriceSum, jobRpsDollars } = CalculateJobRpsDollars(job, true);
|
||||
const { dbPriceSum, jobRpsPc } = CalculateJobRpsPc(job, jobRpsDollars, true);
|
||||
const jobTarget = GetJobTarget({
|
||||
@@ -145,28 +158,30 @@ export function* handleCalculateScoreCard({ payload: jobs }) {
|
||||
v_mileage: job.v_mileage,
|
||||
job: job
|
||||
});
|
||||
scoreCard.shopRpsTotalDollars = scoreCard.shopRpsTotalDollars.add(jobRpsDollars);
|
||||
const expectedRpsDollars = dbPriceSum.percentage(jobTarget * 100);
|
||||
scoreCard.shopRpsExpectedDollars = scoreCard.shopRpsExpectedDollars.add(expectedRpsDollars);
|
||||
if (!isJobExcludedFromCalculation) {
|
||||
scoreCard.shopRpsTotalDollars = scoreCard.shopRpsTotalDollars.add(jobRpsDollars);
|
||||
scoreCard.shopRpsExpectedDollars = scoreCard.shopRpsExpectedDollars.add(expectedRpsDollars);
|
||||
|
||||
scoreCard.allJobsSumDbPrice = scoreCard.allJobsSumDbPrice.add(dbPriceSum);
|
||||
scoreCard.allJobsSumActPrice = scoreCard.allJobsSumActPrice.add(actPriceSum);
|
||||
scoreCard.allJobsSumDbPrice = scoreCard.allJobsSumDbPrice.add(dbPriceSum);
|
||||
scoreCard.allJobsSumActPrice = scoreCard.allJobsSumActPrice.add(actPriceSum);
|
||||
|
||||
const deviationPc = Math.round((jobRpsPc - jobTarget) * 1000) / 10;
|
||||
const deviationPc = Math.round((jobRpsPc - jobTarget) * 1000) / 10;
|
||||
|
||||
scoreCard.scatterChart[job.group].push({
|
||||
deviationPc: isNaN(deviationPc) ? -100 : deviationPc,
|
||||
deviationDollars: (jobRpsDollars.subtract(expectedRpsDollars).getAmount() / 100).toFixed(2),
|
||||
age: job.v_age,
|
||||
dbPriceSum,
|
||||
dbPriceSumAmt: dbPriceSum.getAmount() / 100,
|
||||
id: job.id,
|
||||
owner: `${job.ownr_fn} ${job.ownr_ln}`,
|
||||
vehicle: `${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type}) - ${job.group}`,
|
||||
clm_no: job.clm_no,
|
||||
jobRpsDollars,
|
||||
jobRpsPc: isNaN(jobRpsPc) ? -1 : jobRpsPc
|
||||
});
|
||||
scoreCard.scatterChart[job.group].push({
|
||||
deviationPc: isNaN(deviationPc) ? -100 : deviationPc,
|
||||
deviationDollars: (jobRpsDollars.subtract(expectedRpsDollars).getAmount() / 100).toFixed(2),
|
||||
age: job.v_age,
|
||||
dbPriceSum,
|
||||
dbPriceSumAmt: dbPriceSum.getAmount() / 100,
|
||||
id: job.id,
|
||||
owner: `${job.ownr_fn} ${job.ownr_ln}`,
|
||||
vehicle: `${job.v_model_yr} ${job.v_makedesc} ${job.v_model} (${job.v_type}) - ${job.group}`,
|
||||
clm_no: job.clm_no,
|
||||
jobRpsDollars,
|
||||
jobRpsPc: isNaN(jobRpsPc) ? -1 : jobRpsPc
|
||||
});
|
||||
}
|
||||
|
||||
const jobAlerts = job.joblines
|
||||
.map((jobline) =>
|
||||
@@ -213,5 +228,14 @@ export function* handleCalculateScoreCard({ payload: jobs }) {
|
||||
}
|
||||
|
||||
export function* reportingSagas() {
|
||||
yield all([call(onQueryReportData), call(onSetReportData), call(onCalculateScoreCard), call(onCalculateAudit)]);
|
||||
yield all([
|
||||
call(onQueryReportData),
|
||||
call(onSetReportData),
|
||||
call(onCalculateScoreCard),
|
||||
call(onCalculateAudit),
|
||||
|
||||
call(onClearExcludedIds),
|
||||
call(onAddExcludedIds),
|
||||
call(onRemoveExcludedId)
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const selectReportData = createSelector([selectReporting], (reporting) =>
|
||||
export const selectAuditData = createSelector([selectReporting], (reporting) => reporting.audit);
|
||||
export const selectAuditError = createSelector([selectReporting], (reporting) => reporting.auditError);
|
||||
export const selectAuditLoading = createSelector([selectReporting], (reporting) => reporting.auditLoading);
|
||||
export const selectExcludedIds = createSelector([selectReporting], (reporting) => reporting.excludedIds);
|
||||
// export const selectWatchedPaths = createSelector(
|
||||
// [selectReporting],
|
||||
// (application) => application.watchedPaths
|
||||
|
||||
@@ -7,6 +7,10 @@ const ReportingActionTypes = {
|
||||
TOGGLE_GROUP_VERIFIED: "TOGGLE_GROUP_VERIFIED",
|
||||
CALCULATE_AUDIT: "CALCULATE_AUDIT",
|
||||
SET_AUDIT_RESULTS: "SET_AUDIT_RESULTS",
|
||||
SET_AUDIT_ERROR: "SET_AUDIT_ERROR"
|
||||
SET_AUDIT_ERROR: "SET_AUDIT_ERROR",
|
||||
ADD_EXCLUDED_ID: "ADD_EXCLUDED_ID",
|
||||
REMOVE_EXCLUDED_ID: "REMOVE_EXCLUDED_ID",
|
||||
CLEAR_EXCLUDED_IDS: "CLEAR_EXCLUDED_IDS",
|
||||
SET_CACHED_JOBS: "SET_CACHED_JOBS"
|
||||
};
|
||||
export default ReportingActionTypes;
|
||||
|
||||
Reference in New Issue
Block a user