Files
imexrps/src/redux/reporting/reporting.sagas.js
2025-09-16 14:44:54 -07:00

273 lines
9.4 KiB
JavaScript

import Dinero from "dinero.js";
import _ from "lodash";
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import client from "../../graphql/GraphQLClient";
import { REPORTING_GET_JOBS } from "../../graphql/reporting.queries";
import ipcTypes from "../../ipc.types";
import { CalculateJobRpsDollars, CalculateJobRpsPc } from "../../util/CalculateJobRps";
import GetJobTarget from "../../util/GetJobTarget";
import {
calculateScorecard,
setReportingData,
setScoreCard,
setReportingError,
setAuditResults,
setAuditError,
setCachedJobs
} from "./reporting.actions";
import ReportingApplicationTypes from "./reporting.types";
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,
variables: {
startDate: startDate.format("YYYY-MM-DD"),
endDate: endDate.format("YYYY-MM-DD")
}
});
if (result.errors) {
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));
}
}
export function* onSetReportData() {
yield takeLatest(ReportingApplicationTypes.SET_REPORTING_DATA, handleSetReportData);
}
export function* handleSetReportData({ payload: jobs }) {
// yield put(calculateScorecard(jobs));
}
export function* onCalculateAudit() {
yield takeLatest(ReportingApplicationTypes.CALCULATE_AUDIT, handleCalculateAudit);
}
export function* handleCalculateAudit({ payload: claimsArrayFromAudit }) {
if (claimsArrayFromAudit.length === 0) {
yield put(
setAuditError(
"The excel file did not return any matching results for this sheet. Please ensure you have selected the correct sheet. This is typically 'Shop RPS Claim Detail' but may vary."
)
);
return;
}
const rpsJobs = yield select((state) => state.reporting.data);
//Get List of Claims delta.
const missingFromRps = rpsJobs.filter((job) => !claimsArrayFromAudit.find((c) => c.clm_no.includes(job.clm_no)));
const missingFromAudit = claimsArrayFromAudit.filter((c) => !rpsJobs.find((job) => c.clm_no.includes(job.clm_no)));
//For the items in both spots, highlight the discrepancy.
const claimsArrayHashObject = {};
claimsArrayFromAudit.forEach((claim) => {
const cleansedClaimNo = claim.clm_no.replace(/^0+/, "").trim();
claimsArrayHashObject[cleansedClaimNo] = claim;
});
let expectedMismatch = [];
let actualMismatch = [];
rpsJobs.forEach((rpsJob) => {
const matchingAuditJob = claimsArrayHashObject[rpsJob.clm_no];
if (!matchingAuditJob) {
return;
}
if (Math.abs(rpsJob.expectedRpsDollars.getAmount() / 100 - matchingAuditJob.expected_rps_dollars) > 0.01) {
expectedMismatch.push({ clm_no: rpsJob.clm_no, rps: rpsJob, audit: matchingAuditJob });
}
if (Math.abs(rpsJob.jobRpsDollars.getAmount() / 100 - matchingAuditJob.actual_rps_dollars) > 0.01) {
actualMismatch.push({ clm_no: rpsJob.clm_no, rps: rpsJob, audit: matchingAuditJob });
}
});
yield put(setAuditResults({ missingFromRps, missingFromAudit, expectedMismatch, actualMismatch }));
}
export function* onCalculateScoreCard() {
yield takeLatest(ReportingApplicationTypes.CALCULATE_SCORE_CARD, handleCalculateScoreCard);
}
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 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)
.map((j) => {
return { ...j, error: "No group set." };
});
if (jobsWithNoGroup.length > 0) {
yield put(
setReportingError({
message: "There is an issue with the following jobs.",
jobs: [...jobsWithNoGroup]
})
);
return;
}
const scoreCard = {
shopRpsTotalDollars: Dinero(),
shopRpsExpectedDollars: Dinero(),
varianceDollars: null,
variancePc: 0,
allJobsSumDbPrice: Dinero(),
allJobsSumActPrice: Dinero(),
currentRpsPc: 0,
targetRpsPc: 0,
scatterChart: _.sortBy(_.uniq(jobs.map((j) => j.group)), [(group) => group.toLowerCase()], ["desc"]).reduce(
(acc, val) => {
return { ...acc, [val]: [] };
},
{}
)
};
const bodyshop = yield select((state) => state.user.bodyshop);
//Get the RPS on a per job basis.
jobs = jobs.map((job) => {
const isJobExcludedFromCalculation = excludedJobIds.includes(job.id);
const { actPriceSum, jobRpsDollars } = CalculateJobRpsDollars(job, true, bodyshop.mpi_count_quantity);
const { dbPriceSum, jobRpsPc } = CalculateJobRpsPc(job, jobRpsDollars, true, bodyshop.mpi_count_quantity);
const jobTarget = GetJobTarget({
group: job.group,
v_age: job.v_age,
targets,
close_date: job.close_date,
v_mileage: job.v_mileage,
job: job
});
const expectedRpsDollars = dbPriceSum.percentage(jobTarget * 100);
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);
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
});
}
const simpleJobAlerts = [];
job.joblines.forEach((jobline) => {
if (jobline.ignore) {
return;
}
if (jobline.part_qty > 1) {
simpleJobAlerts.push({
key: `line-${jobline.id}-q`,
alert: `Line ${jobline.line_no} has a quantity greater than 1 (${jobline.part_qty})`,
line_no: jobline.line_no,
line_desc: jobline.line_desc,
id: jobline.id
});
}
if (jobline.price_diff < 0 && !jobline.db_ref?.startsWith("9005")) {
simpleJobAlerts.push({
key: `line-${jobline.id}-p`,
alert: `Line ${jobline.line_no} has negative savings (${jobline.price_diff})`,
line_no: jobline.line_no,
line_desc: jobline.line_desc,
id: jobline.id
});
}
});
const jobAlerts = [...simpleJobAlerts,
// ...job.joblines
// .map((jobline) =>
// jobline.alerts?.map((alert, idx) => ({
// key: idx,
// label: `Line ${jobline.line_no}: ${alert.key}`,
// children: alert.alert
// // style: {
// // backgroundColor: token.colorErrorBgHover
// // }
// }))
// )
// .flat()
];
//sum db price * percentage expected.
return {
...job,
alerts: jobAlerts,
actPriceSum,
jobRpsDollars,
dbPriceSum,
jobRpsPc,
jobTarget,
expectedRpsDollars
};
});
scoreCard.varianceDollars = scoreCard.shopRpsTotalDollars.subtract(scoreCard.shopRpsExpectedDollars);
scoreCard.currentRpsPc = scoreCard.shopRpsTotalDollars.getAmount() / scoreCard.allJobsSumDbPrice.getAmount();
scoreCard.targetRpsPc = scoreCard.shopRpsExpectedDollars.getAmount() / scoreCard.allJobsSumDbPrice.getAmount();
scoreCard.variancePc = scoreCard.currentRpsPc - scoreCard.targetRpsPc;
//Set the data.
yield put(setScoreCard(scoreCard));
yield put(setReportingData(jobs));
} catch (error) {
ipcRenderer.send(ipcTypes.app.toMain.track, {
event: "CALCULATE_SCORE_CARD_ERROR",
error: error
});
yield put(setReportingError({ message: error, jobs: [] }));
}
}
export function* reportingSagas() {
yield all([
call(onQueryReportData),
call(onSetReportData),
call(onCalculateScoreCard),
call(onCalculateAudit),
call(onClearExcludedIds),
call(onAddExcludedIds),
call(onRemoveExcludedId)
]);
}