import moment from "moment"; 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: Moment.js is not immutable. Today WILL change when adjusted. const today = moment().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 && moment(item.scheduled_completion).isBefore(moment().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 && moment(item.actual_completion).isBefore(moment().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 && moment(item.actual_in).isAfter(moment())) { problemJobs.push({ ...item, code: "Job has an actual in date set in the future", }); } if ( item.actual_completion && moment(item.actual_completion).isAfter(moment()) ) { 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 = moment(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; //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; } } 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, hoursIn: AddJobForSchedulingCalc ? item.labhrs.aggregate.sum.mod_lb_hrs + 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 = moment( 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; //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; } } 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, }; } }); //Propagate the expected load to each day. const range = Math.round(moment.duration(end.diff(today)).asDays()) + 1; for (var day = 0; day < range; day++) { const current = moment(today).add(day, "days").format("yyyy-MM-DD"); const prev = moment(today) .add(day - 1, "days") .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)]); }