Initial scenario manager for reporting.

This commit is contained in:
Patrick Fic
2025-02-19 12:08:44 -08:00
parent d044cce054
commit 0cea35ba24
7 changed files with 191 additions and 100 deletions

View File

@@ -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);
}}
/>
)
}
];

View File

@@ -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>
)}
</>
);
}

View File

@@ -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 });

View File

@@ -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,

View File

@@ -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)
]);
}

View File

@@ -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

View File

@@ -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;