const path = require("path"); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); const moment = require("moment-timezone"); const logger = require("../utils/logger"); const _ = require("lodash"); const { filter } = require("lodash"); require("dotenv").config({ path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); exports.job = async (req, res) => { const { jobId } = req.body; const BearerToken = req.BearerToken; const client = req.userGraphQLClient; try { logger.log("smart-scheduling-start", "DEBUG", req.user.email, jobId, null); const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_UPCOMING_APPOINTMENTS, { now: moment().startOf("day"), jobId: jobId }); const { jobs_by_pk, blockedDays, prodJobs, arrJobs, compJobs } = result; const { ssbuckets, workingdays, timezone, ss_configuration } = result.jobs_by_pk.bodyshop; const jobHrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs; const JobBucket = ssbuckets.filter( (bucket) => bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true) )[0]; const load = { productionTotal: {}, productionHours: 0 }; //Set the current load. ssbuckets.forEach((bucket) => { load.productionTotal[bucket.id] = { count: 0, label: bucket.label }; }); const filteredProdJobsList = prodJobs.filter((j) => JobBucket.id === CheckJobBucket(ssbuckets, j)); filteredProdJobsList.forEach((item) => { //Add all of the jobs currently in production to the buckets so that we have a starting point. const bucketId = CheckJobBucket(ssbuckets, item); 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); } }); // const filteredArrJobs = arrJobs.filter( // (j) => JobBucket.id === CheckJobBucket(ssbuckets, j) // ); const filteredArrJobs = []; arrJobs.forEach((item) => { let isSameBucket = false; if (JobBucket.id === CheckJobBucket(ssbuckets, item)) { filteredArrJobs.push(item); isSameBucket = true; } let jobHours = item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs; const AddJobForSchedulingCalc = !item.inproduction; const itemDate = moment(item.actual_in || item.scheduled_in) .tz(timezone) .format("yyyy-MM-DD"); if (isSameBucket) { if (!!load[itemDate]) { load[itemDate].hoursIn = (load[itemDate].hoursIn || 0) + AddJobForSchedulingCalc ? jobHours : 0; if (AddJobForSchedulingCalc) load[itemDate].jobsIn.push(item); } else { load[itemDate] = { jobsIn: AddJobForSchedulingCalc ? [item] : [], jobsOut: [], hoursIn: AddJobForSchedulingCalc ? jobHours : 0 }; } } if (!load[itemDate]) { load[itemDate] = { jobsIn: [], jobsOut: [], hoursIn: 0, hoursInTotal: 0 }; } load[itemDate].hoursInTotal = (load[itemDate].hoursInTotal || 0) + jobHours; }); //Get the completing jobs. let problemJobs = []; const filteredCompJobs = compJobs.filter((j) => JobBucket.id === CheckJobBucket(ssbuckets, j)); filteredCompJobs.forEach((item) => { const inProdJobs = filteredProdJobsList.find((p) => p.id === item.id); const inArrJobs = filteredArrJobs.find((p) => p.id === item.id); const AddJobForSchedulingCalc = inProdJobs || inArrJobs; const itemDate = moment(item.actual_completion || item.scheduled_completion) .tz(timezone) .format("yyyy-MM-DD"); if (!!load[itemDate]) { load[itemDate].hoursOut = (load[itemDate].hoursOut || 0) + AddJobForSchedulingCalc ? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs : 0; if (AddJobForSchedulingCalc) load[itemDate].jobsOut.push(item); } else { load[itemDate] = { jobsOut: AddJobForSchedulingCalc ? [item] : [], hoursOut: AddJobForSchedulingCalc ? item.labhrs.aggregate.sum.mod_lb_hrs + item.larhrs.aggregate.sum.mod_lb_hrs : 0 }; } }); //Propagate the expected load to each day. const yesterday = moment().tz(timezone).subtract(1, "day"); const today = moment().tz(timezone); const end = moment.max([ ...filteredArrJobs.map((a) => moment(a.scheduled_in).tz(timezone)), ...filteredCompJobs .map((p) => moment(p.actual_completion || p.scheduled_completion).tz(timezone)) .filter((p) => p.isValid() && p.isAfter(yesterday)), moment().tz(timezone).add(15, "days") ]); const range = Math.round(moment.duration(end.add(20, "days").diff(today)).asDays()); for (var day = 0; day < range; day++) { const current = moment(today).tz(timezone).add(day, "days").format("yyyy-MM-DD"); const prev = moment(today) .tz(timezone) .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, ssbuckets, load[current].jobsIn || [], load[current].jobsOut || [] ); } else { load[current].expectedLoad = CalculateLoad( load[prev].expectedLoad, ssbuckets, load[current].jobsIn || [], load[current].jobsOut || [] ); } } //Add in all of the blocked days. blockedDays.forEach((b) => { //Find it in the load, set it as blocked. const startIsoFormat = moment(b.start).tz(timezone).format("YYYY-MM-DD"); if (load[startIsoFormat]) load[startIsoFormat].blocked = true; else { load[startIsoFormat] = { blocked: true }; } }); // //Propose the first 10 dates where we are below target. const possibleDates = []; delete load.productionTotal; const loadKeys = Object.keys(load).sort((a, b) => (moment(a).isAfter(moment(b)) ? 1 : -1)); loadKeys.forEach((loadKey) => { const isShopOpen = (workingdays[dayOfWeekMapper(moment(loadKey).day())] || false) && !load[loadKey].blocked; let isUnderDailyTotalLimit = true; if ( ss_configuration && ss_configuration.dailyhrslimit && ss_configuration.dailyhrslimit > 0 && load[loadKey] && load[loadKey].hoursInTotal && load[loadKey].hoursInTotal > ss_configuration.dailyhrslimit ) { isUnderDailyTotalLimit = false; } if ( load[loadKey].expectedLoad && load[loadKey].expectedLoad[JobBucket.id] && JobBucket.target > load[loadKey].expectedLoad[JobBucket.id].count && isShopOpen && isUnderDailyTotalLimit ) possibleDates.push(new Date(loadKey).toISOString().substr(0, 10)); }); if (possibleDates.length < 11) { res.json(possibleDates); } else { res.json(possibleDates.slice(0, 10)); } } catch (error) { logger.log("smart-scheduling-error", "ERROR", req.user.email, jobId, { error }); res.status(400).send(error); } }; const dayOfWeekMapper = (numberOfDay) => { switch (numberOfDay) { case 0: return "sunday"; case 1: return "monday"; case 2: return "tuesday"; case 3: return "wednesday"; case 4: return "thursday"; case 5: return "friday"; case 6: return "saturday"; } }; const CheckJobBucket = (buckets, job) => { const jobHours = job.labhrs.aggregate.sum.mod_lb_hrs + job.larhrs.aggregate.sum.mod_lb_hrs; const matchingBucket = buckets.filter((b) => (b.gte <= jobHours && b.lt ? b.lt > jobHours : true)); return matchingBucket[0] && matchingBucket[0].id; }; const CalculateLoad = (currentLoad, buckets, jobsIn, jobsOut) => { //Add the jobs coming const newLoad = _.cloneDeep(currentLoad); jobsIn.forEach((job) => { const bucketId = CheckJobBucket(buckets, job); if (bucketId) { newLoad[bucketId].count = newLoad[bucketId].count + 1; } else { // console.log("[Util Arr Job]Uh oh, this job doesn't fit in a bucket!", job); } }); jobsOut.forEach((job) => { const bucketId = CheckJobBucket(buckets, job); if (bucketId) { newLoad[bucketId].count = newLoad[bucketId].count - 1; if (newLoad[bucketId].count < 0) { // console.log("***ERROR: NEGATIVE LOAD Bucket =>", bucketId, job); } } else { // console.log("[Util Out Job]Uh oh, this job doesn't fit in a bucket!", job); } }); return newLoad; };