273 lines
9.4 KiB
JavaScript
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)
|
|
]);
|
|
}
|