275 lines
8.7 KiB
JavaScript
275 lines
8.7 KiB
JavaScript
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;
|
|
};
|