From 0cea35ba240c3db2da8ceab6c6341a3b93c4dc43 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 19 Feb 2025 12:08:44 -0800 Subject: [PATCH] Initial scenario manager for reporting. --- .../reporting-jobs-list.molecule.jsx | 41 ++++- .../reporting-totals-stats.molecule.jsx | 145 +++++++++--------- src/redux/reporting/reporting.actions.js | 16 ++ src/redux/reporting/reporting.reducer.js | 12 +- src/redux/reporting/reporting.sagas.js | 70 ++++++--- src/redux/reporting/reporting.selectors.js | 1 + src/redux/reporting/reporting.types.js | 6 +- 7 files changed, 191 insertions(+), 100 deletions(-) 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 index d69a88a..fb6d869 100644 --- a/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx +++ b/src/components/molecules/reporting-jobs-list/reporting-jobs-list.molecule.jsx @@ -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 && ( - + )} @@ -134,6 +151,20 @@ export function ReportingJobsListMolecule({ scoreCard, reportingLoading, reportD {`${(record.jobRpsPc * 100 || 0).toFixed(1)}% / ${(record.jobTarget * 100).toFixed(1)}%`} ) + }, + { + title: "Temporarily Exclude", + dataIndex: "scenario_manager", + key: "scenario_manager", + render: (text, record) => ( + { + console.log("Chcekedd", checked); + checked ? addExcludedId(record.id) : removeExcludedId(record.id); + }} + /> + ) } ]; 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 index 1560970..5424ae0 100644 --- a/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx +++ b/src/components/molecules/reporting-totals-stats/reporting-totals-stats.molecule.jsx @@ -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 ; - if (!scoreCard) - return ; + if (!scoreCard) return ; return ( -
-
- - - + <> +
+
+ + + +
+
+ + + +
-
- 0 && ( + clearExcludedIds()}> + Clear + + } + message={`There are ${excludedJobIds.length} jobs excluded from the totals above.`} /> - - -
-
+ )} + ); } diff --git a/src/redux/reporting/reporting.actions.js b/src/redux/reporting/reporting.actions.js index b89f82e..200c7a7 100644 --- a/src/redux/reporting/reporting.actions.js +++ b/src/redux/reporting/reporting.actions.js @@ -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 }); diff --git a/src/redux/reporting/reporting.reducer.js b/src/redux/reporting/reporting.reducer.js index 7593615..609d897 100644 --- a/src/redux/reporting/reporting.reducer.js +++ b/src/redux/reporting/reporting.reducer.js @@ -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, diff --git a/src/redux/reporting/reporting.sagas.js b/src/redux/reporting/reporting.sagas.js index af8eaa6..767594c 100644 --- a/src/redux/reporting/reporting.sagas.js +++ b/src/redux/reporting/reporting.sagas.js @@ -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) + ]); } diff --git a/src/redux/reporting/reporting.selectors.js b/src/redux/reporting/reporting.selectors.js index 2427308..585e223 100644 --- a/src/redux/reporting/reporting.selectors.js +++ b/src/redux/reporting/reporting.selectors.js @@ -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 diff --git a/src/redux/reporting/reporting.types.js b/src/redux/reporting/reporting.types.js index e7b87c4..fa2fc08 100644 --- a/src/redux/reporting/reporting.types.js +++ b/src/redux/reporting/reporting.types.js @@ -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;