import dayjs from "../../utils/day"; import { all, call, put, select, takeLatest } from "redux-saga/effects"; import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries"; import { INSERT_AUDIT_TRAIL } from "../../graphql/audit_trail.queries"; import client from "../../utils/GraphQLClient"; import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils"; import { scheduleLoadFailure, scheduleLoadSuccess, setProblemJobs } from "./application.actions"; import ApplicationActionTypes from "./application.types"; export function* onCalculateScheduleLoad() { yield takeLatest(ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD, calculateScheduleLoad); } export function* calculateScheduleLoad({ payload: end }) { //REMINDER: dayjs.js is not immutable. Today WILL change when adjusted. const today = dayjs().startOf("day"); const state = yield select(); const buckets = state.user.bodyshop.ssbuckets; try { const result = yield client.query({ query: QUERY_SCHEDULE_LOAD_DATA, variables: { start: today, end: end } }); const { prodJobs, arrJobs, compJobs } = result.data; const load = { productionTotal: {}, productionHours: 0 }; let problemJobs = []; //Set the current load. buckets.forEach((bucket) => { load.productionTotal[bucket.id] = { count: 0, label: bucket.label }; }); prodJobs.forEach((item) => { //Add all of the jobs currently in production to the buckets so that we have a starting point. if (!item.actual_completion && dayjs(item.scheduled_completion).isBefore(dayjs().startOf("day"))) { problemJobs.push({ ...item, code: "Job was scheduled to go, but it has not been completed. Update the scheduled completion date to correct projections" }); } if (item.actual_completion && dayjs(item.actual_completion).isBefore(dayjs().startOf("day"))) { problemJobs.push({ ...item, code: "Job is already marked as completed, but it is still in production. This job should be removed from production" }); } if (!(item.actual_completion || item.scheduled_completion)) { problemJobs.push({ ...item, code: "Job does not have a scheduled or actual completion date. Update the scheduled or actual completion dates to correct projections" }); } const bucketId = CheckJobBucket(buckets, item); load.productionHours = load.productionHours + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; if (bucketId) { load.productionTotal[bucketId].count = load.productionTotal[bucketId].count + 1; } else { console.log("Uh oh, this job doesn't fit in a bucket!", item); } }); arrJobs.forEach((item) => { if (!item.scheduled_in) { console.log("JOB HAS NO SCHEDULED IN DATE.", item); problemJobs.push({ ...item, code: "Job has no scheduled in date" }); } if (!item.actual_completion && item.actual_in && !item.inproduction) { problemJobs.push({ ...item, code: "Job has an actual in date, but no actual completion date and is not marked as in production" }); } if (item.actual_in && dayjs(item.actual_in).isAfter(dayjs())) { problemJobs.push({ ...item, code: "Job has an actual in date set in the future" }); } if (item.actual_completion && dayjs(item.actual_completion).isAfter(dayjs())) { problemJobs.push({ ...item, code: "Job has an actual completion date set in the future" }); } if (item.actual_completion && item.inproduction) { problemJobs.push({ ...item, code: "Job has an actual completion date but it is still marked in production" }); } const itemDate = dayjs(item.actual_in || item.scheduled_in).format("YYYY-MM-DD"); const AddJobForSchedulingCalc = !item.inproduction; if (!!load[itemDate]) { load[itemDate].allHoursIn = (load[itemDate].allHoursIn || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; load[itemDate].allHoursInBody = (load[itemDate].allHoursInBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs; load[itemDate].allHoursInRefinish = (load[itemDate].allHoursInRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs; //If the job hasn't already arrived, add it to the jobs in list. // Make sure it also hasn't already been completed, or isn't an in and out job. //This prevents the duplicate counting. load[itemDate].allJobsIn.push(item); if (AddJobForSchedulingCalc) { load[itemDate].jobsIn.push(item); load[itemDate].hoursIn = (load[itemDate].hoursIn || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; load[itemDate].hoursInBody = (load[itemDate].hoursInBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs; load[itemDate].hoursInRefinish = (load[itemDate].hoursInRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs; } } else { load[itemDate] = { allJobsIn: [item], jobsIn: AddJobForSchedulingCalc ? [item] : [], //Same as above, only add it if it isn't already in production. jobsOut: [], allJobsOut: [], allHoursIn: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs, allHoursInBody: item.labhrs.aggregate.sum.mod_lb_hrs, allHoursInRefinish: item.larhrs.aggregate.sum.mod_lb_hrs, hoursIn: AddJobForSchedulingCalc ? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs : 0, hoursInBody: AddJobForSchedulingCalc ? item.labhrs.aggregate.sum.mod_lb_hrs : 0, hoursInRefinish: AddJobForSchedulingCalc ? item.larhrs.aggregate.sum.mod_lb_hrs : 0, }; } }); compJobs.forEach((item) => { if (!(item.actual_completion || item.scheduled_completion)) console.warn("JOB HAS NO COMPLETION DATE.", item); const inProdJobs = prodJobs.find((p) => p.id === item.id); const inArrJobs = arrJobs.find((p) => p.id === item.id); const AddJobForSchedulingCalc = inProdJobs || inArrJobs; const itemDate = dayjs(item.actual_completion || item.scheduled_completion).format("YYYY-MM-DD"); //Skip it, it's already completed. if (!!load[itemDate]) { load[itemDate].allHoursOut = (load[itemDate].allHoursOut || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; load[itemDate].allHoursOutBody = (load[itemDate].allHoursOutBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs; load[itemDate].allHoursOutRefinish = (load[itemDate].allHoursOutRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs; //Add only the jobs that are still in production to get rid of. //If it's not in production, we'd subtract unnecessarily. load[itemDate].allJobsOut.push(item); if (AddJobForSchedulingCalc) { load[itemDate].jobsOut.push(item); load[itemDate].hoursOut = (load[itemDate].hoursOut || 0) + item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; load[itemDate].hoursOutBody = (load[itemDate].hoursOutBody || 0) + item.labhrs.aggregate.sum.mod_lb_hrs; load[itemDate].hoursOutRefinish = (load[itemDate].hoursOutRefinish || 0) + item.larhrs.aggregate.sum.mod_lb_hrs; } } else { load[itemDate] = { allJobsOut: [item], jobsOut: AddJobForSchedulingCalc ? [item] : [], //Same as above. hoursOut: AddJobForSchedulingCalc ? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs : 0, allHoursOut: item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs, allHoursOutBody: item.labhrs.aggregate.sum.mod_lb_hrs, allHoursOutRefinish: item.larhrs.aggregate.sum.mod_lb_hrs, }; } }); //Propagate the expected load to each day. const range = Math.round(dayjs.duration(end.diff(today)).asDays()) + 1; for (var day = 0; day < range; day++) { const current = dayjs(today).add(day, "day").format("YYYY-MM-DD"); const prev = dayjs(today) .add(day - 1, "day") .format("YYYY-MM-DD"); if (!!!load[current]) { load[current] = {}; } if (day === 0) { //Starting on day 1. The load is current. load[current].expectedLoad = CalculateLoad( load.productionTotal, buckets, load[current].jobsIn || [], load[current].jobsOut || [] ); load[current].expectedJobCount = prodJobs.length + (load[current].jobsIn || []).length - (load[current].jobsOut || []).length; load[current].expectedHours = load.productionHours + (load[current].hoursIn || 0) - (load[current].hoursOut || 0); } else { load[current].expectedLoad = CalculateLoad( load[prev].expectedLoad, buckets, load[current].jobsIn || [], load[current].jobsOut || [] ); load[current].expectedJobCount = load[prev].expectedJobCount + (load[current].jobsIn || []).length - (load[current].jobsOut || []).length; load[current].expectedHours = load[prev].expectedHours + (load[current].hoursIn || 0) - (load[current].hoursOut || 0); } } yield put(setProblemJobs(problemJobs)); yield put(scheduleLoadSuccess(load)); } catch (error) { yield put(scheduleLoadFailure(error)); } } export function* onInsertAuditTrail() { yield takeLatest(ApplicationActionTypes.INSERT_AUDIT_TRAIL, insertAuditTrailSaga); } export function* insertAuditTrailSaga({ payload: { jobid, billid, operation, type } }) { const state = yield select(); const bodyshop = state.user.bodyshop; const currentUser = state.user.currentUser; const variables = { auditObj: { bodyshopid: bodyshop.id, jobid, billid, operation, type, useremail: currentUser.email } }; yield client.mutate({ mutation: INSERT_AUDIT_TRAIL, variables, update(cache, { data }) { cache.modify({ fields: { audit_trail(existingAuditTrail, { readField }) { const newAuditTrail = cache.writeQuery({ data: data, query: INSERT_AUDIT_TRAIL, variables }); return [...existingAuditTrail, newAuditTrail]; } } }); } }); } export function* applicationSagas() { yield all([call(onCalculateScheduleLoad), call(onInsertAuditTrail)]); }