IO-1605 Refactor smart scheduling.
This commit is contained in:
@@ -117,9 +117,9 @@
|
||||
"devDependencies": {
|
||||
"@sentry/webpack-plugin": "^1.18.3",
|
||||
"@testing-library/cypress": "^8.0.2",
|
||||
"react-error-overlay": "6.0.9",
|
||||
"cypress": "^9.1.1",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"react-error-overlay": "6.0.9",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
}
|
||||
|
||||
@@ -10,13 +10,17 @@ import Event from "../job-at-change/schedule-event.container";
|
||||
import HeaderComponent from "./schedule-calendar-header.component";
|
||||
import "./schedule-calendar.styles.scss";
|
||||
import JobDetailCards from "../job-detail-cards/job-detail-cards.component";
|
||||
import { selectProblemJobs } from "../../redux/application/application.selectors";
|
||||
import { Alert } from "antd";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
problemJobs: selectProblemJobs,
|
||||
});
|
||||
const localizer = momentLocalizer(moment);
|
||||
export function ScheduleCalendarWrapperComponent({
|
||||
bodyshop,
|
||||
problemJobs,
|
||||
data,
|
||||
refetch,
|
||||
defaultView,
|
||||
@@ -48,6 +52,15 @@ export function ScheduleCalendarWrapperComponent({
|
||||
return (
|
||||
<>
|
||||
<JobDetailCards />
|
||||
{problemJobs &&
|
||||
problemJobs.map((problem) => (
|
||||
<Alert
|
||||
key={problem.id}
|
||||
type="error"
|
||||
message={`${problem.ro_number} has a data consistency issue. It has been exlcuded for scheduling purposes. CODE: ${problem.code}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Calendar
|
||||
events={data}
|
||||
defaultView={search.view || defaultView || "week"}
|
||||
|
||||
@@ -5,6 +5,7 @@ import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/sched
|
||||
import ScheduleModal from "../schedule-job-modal/schedule-job-modal.container";
|
||||
//import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||
import ScheduleProductionList from "../schedule-production-list/schedule-production-list.component";
|
||||
import ScheduleVerifyIntegrity from "../schedule-verify-integrity/schedule-verify-integrity.component";
|
||||
|
||||
export default function ScheduleCalendarComponent({ data, refetch }) {
|
||||
return (
|
||||
@@ -15,6 +16,7 @@ export default function ScheduleCalendarComponent({ data, refetch }) {
|
||||
<PageHeader
|
||||
extra={
|
||||
<Space wrap>
|
||||
<ScheduleVerifyIntegrity />
|
||||
<Button
|
||||
onClick={() => {
|
||||
refetch();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { Button } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_SCHEDULE_LOAD_DATA } from "../../graphql/appointments.queries";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScheduleVerifyIntegrity);
|
||||
|
||||
export function ScheduleVerifyIntegrity({ currentUser }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const client = useApolloClient();
|
||||
const handleVerify = async () => {
|
||||
setLoading(true);
|
||||
const {
|
||||
data: { arrJobs, compJobs, prodJobs },
|
||||
} = await client.query({
|
||||
query: QUERY_SCHEDULE_LOAD_DATA,
|
||||
variables: { start: moment(), end: moment().add(180, "days") },
|
||||
});
|
||||
|
||||
//check that the leaving jobs are either in the arriving list, or in production.
|
||||
const issues = [];
|
||||
|
||||
compJobs.forEach((j) => {
|
||||
const inProdJobs = prodJobs.find((p) => p.id === j.id);
|
||||
const inArrJobs = arrJobs.find((p) => p.id === j.id);
|
||||
|
||||
if (!(inProdJobs || inArrJobs)) {
|
||||
// NOT FOUND!
|
||||
issues.push(j);
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
"The following completing jobs are not in production, or are arriving within the next 180 days. ",
|
||||
issues
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (currentUser.email === "patrick@imex.prod")
|
||||
return (
|
||||
<Button loading={loading} onClick={handleVerify}>
|
||||
Developer Use Only - Verify Schedule Integrity
|
||||
</Button>
|
||||
);
|
||||
else return null;
|
||||
}
|
||||
@@ -234,11 +234,17 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
}
|
||||
}
|
||||
compJobs: jobs(
|
||||
where: { scheduled_completion: { _gte: $start, _lte: $end } }
|
||||
where: {
|
||||
_or: [
|
||||
{ scheduled_completion: { _gte: $start, _lte: $end } }
|
||||
{ actual_completion: { _gte: $start, _lte: $end } }
|
||||
]
|
||||
}
|
||||
) {
|
||||
id
|
||||
ro_number
|
||||
scheduled_completion
|
||||
actual_completion
|
||||
labhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
|
||||
@@ -58,3 +58,7 @@ export const insertAuditTrail = ({ jobid, billid, operation }) => ({
|
||||
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
|
||||
payload: { jobid, billid, operation },
|
||||
});
|
||||
export const setProblemJobs = (problemJobs) => ({
|
||||
type: ApplicationActionTypes.SET_PROBLEM_JOBS,
|
||||
payload: problemJobs,
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ const INITIAL_STATE = {
|
||||
breadcrumbs: [],
|
||||
recentItems: [],
|
||||
selectedHeader: "home",
|
||||
problemJobs: [],
|
||||
scheduleLoad: {
|
||||
load: {},
|
||||
calculating: false,
|
||||
@@ -40,6 +41,7 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD:
|
||||
return {
|
||||
...state,
|
||||
problemJobs: [],
|
||||
scheduleLoad: { ...state.scheduleLoad, calculating: true, error: null },
|
||||
};
|
||||
case ApplicationActionTypes.CALCULATE_SCHEDULE_LOAD_SUCCESS:
|
||||
@@ -76,7 +78,9 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
|
||||
case ApplicationActionTypes.SET_PARTNER_VERSION:
|
||||
return { ...state, partnerVersion: action.payload };
|
||||
|
||||
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
|
||||
return { ...state, problemJobs: action.payload };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CalculateLoad, CheckJobBucket } from "../../utils/SSSUtils";
|
||||
import {
|
||||
scheduleLoadFailure,
|
||||
scheduleLoadSuccess,
|
||||
setProblemJobs,
|
||||
} from "./application.actions";
|
||||
import ApplicationActionTypes from "./application.types";
|
||||
|
||||
@@ -53,6 +54,8 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
});
|
||||
|
||||
arrJobs.forEach((item) => {
|
||||
if (!item.scheduled_in)
|
||||
console.log("JOB HAS NO SCHEDULED IN DATE.", item);
|
||||
const itemDate = moment(item.scheduled_in).format("yyyy-MM-DD");
|
||||
if (!!load[itemDate]) {
|
||||
load[itemDate].hoursIn =
|
||||
@@ -71,7 +74,26 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
}
|
||||
});
|
||||
|
||||
let problemJobs = [];
|
||||
compJobs.forEach((item) => {
|
||||
if (!item.scheduled_completion)
|
||||
console.log("JOB HAS NO SCHEDULED COMPLETION DATE.", item);
|
||||
|
||||
const inProdJobs = prodJobs.find((p) => p.id === item.id);
|
||||
const inArrJobs = arrJobs.find((p) => p.id === item.id);
|
||||
|
||||
if (
|
||||
!(inProdJobs || inArrJobs) &&
|
||||
!moment(item.scheduled_completion).isSame(moment(), "day")
|
||||
) {
|
||||
// NOT FOUND!
|
||||
problemJobs.push({
|
||||
...item,
|
||||
code: "Job is scheduled for completion, but it is not marked in production nor is it an arriving job in this period. Check the scheduled in and completion dates",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const itemDate = moment(item.scheduled_completion).format("yyyy-MM-DD");
|
||||
if (!!load[itemDate]) {
|
||||
load[itemDate].hoursOut =
|
||||
@@ -116,7 +138,7 @@ export function* calculateScheduleLoad({ payload: end }) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
yield put(setProblemJobs(problemJobs));
|
||||
yield put(scheduleLoadSuccess(load));
|
||||
} catch (error) {
|
||||
yield put(scheduleLoadFailure(error));
|
||||
|
||||
@@ -44,3 +44,7 @@ export const selectOnline = createSelector(
|
||||
[selectApplication],
|
||||
(application) => application.online
|
||||
);
|
||||
export const selectProblemJobs = createSelector(
|
||||
[selectApplication],
|
||||
(application) => application.problemJobs
|
||||
);
|
||||
|
||||
@@ -11,5 +11,6 @@ const ApplicationActionTypes = {
|
||||
SET_PARTNER_VERSION: "SET_PARTNER_VERSION",
|
||||
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
|
||||
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
|
||||
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
||||
};
|
||||
export default ApplicationActionTypes;
|
||||
|
||||
@@ -30,6 +30,9 @@ export const CalculateLoad = (currentLoad, buckets, jobsIn, jobsOut) => {
|
||||
const bucketId = CheckJobBucket(buckets, job);
|
||||
if (bucketId) {
|
||||
newLoad[bucketId].count = newLoad[bucketId].count - 1;
|
||||
if (newLoad[bucketId].count < 0) {
|
||||
console.log("***ERROR: NEGATIVE LOAD", bucketId, job);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"[Util Out Job]Uh oh, this job doesn't fit in a bucket!",
|
||||
|
||||
@@ -454,15 +454,14 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports.QUERY_UPCOMING_APPOINTMENTS = `
|
||||
query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
|
||||
exports.QUERY_UPCOMING_APPOINTMENTS = `query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
|
||||
jobs_by_pk(id: $jobId) {
|
||||
bodyshop {
|
||||
ssbuckets
|
||||
target_touchtime
|
||||
workingdays
|
||||
}
|
||||
jobhrs: joblines_aggregate(where: { removed: { _eq: false } }) {
|
||||
jobhrs: joblines_aggregate(where: {removed: {_eq: false}}) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
@@ -470,40 +469,60 @@ query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
appointments(where: {_and: {canceled: {_eq: false}, start: {_gte: $now}}}) {
|
||||
blockedDays: appointments(where: {_and: {canceled: {_eq: false}, block: {_eq: true}, start: {_gte: $now}}}) {
|
||||
start
|
||||
isintake
|
||||
id
|
||||
block
|
||||
job {
|
||||
joblines_aggregate(where: { removed: { _eq: false } }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
arrJobs: jobs(where: {scheduled_in: {_gte: $now}}) {
|
||||
id
|
||||
scheduled_in
|
||||
ro_number
|
||||
labhrs: joblines_aggregate(where: {mod_lbr_ty: {_neq: "LAR"}, removed: {_eq: false}}) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: {mod_lbr_ty: {_eq: "LAR"}, removed: {_eq: false}}) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jobs(where: {inproduction: {_eq: true}}) {
|
||||
compJobs: jobs(where: {_or: [{scheduled_completion: {_gte: $now}}, {actual_completion: {_gte: $now}}]}) {
|
||||
id
|
||||
ro_number
|
||||
scheduled_completion
|
||||
actual_completion
|
||||
labhrs: joblines_aggregate(where: {mod_lbr_ty: {_neq: "LAR"}, removed: {_eq: false}}) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: {mod_lbr_ty: {_eq: "LAR"}, removed: {_eq: false}}) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prodJobs: jobs(where: {inproduction: {_eq: true}}) {
|
||||
id
|
||||
scheduled_completion
|
||||
labhrs: joblines_aggregate(
|
||||
where: {
|
||||
_and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }]
|
||||
}
|
||||
) {
|
||||
labhrs: joblines_aggregate(where: {_and: [{mod_lbr_ty: {_neq: "LAR"}}, {removed: {_eq: false}}]}) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(
|
||||
where: {
|
||||
_and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }]
|
||||
}
|
||||
) {
|
||||
larhrs: joblines_aggregate(where: {_and: [{mod_lbr_ty: {_eq: "LAR"}}, {removed: {_eq: false}}]}) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
@@ -512,6 +531,9 @@ query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
`;
|
||||
|
||||
exports.QUERY_EMPLOYEE_PIN = `query QUERY_EMPLOYEE_PIN($shopId: uuid!, $employeeId: String!) {
|
||||
|
||||
@@ -4,6 +4,7 @@ const queries = require("../graphql-client/queries");
|
||||
const Dinero = require("dinero.js");
|
||||
const moment = require("moment");
|
||||
const logger = require("../utils/logger");
|
||||
const _ = require("lodash");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
@@ -30,7 +31,7 @@ exports.job = async (req, res) => {
|
||||
jobId: jobId,
|
||||
});
|
||||
|
||||
const { appointments, jobs } = result;
|
||||
const { jobs_by_pk, blockedDays, prodJobs, arrJobs, compJobs } = result;
|
||||
const { ssbuckets, workingdays } = result.jobs_by_pk.bodyshop;
|
||||
const jobHrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs;
|
||||
|
||||
@@ -39,105 +40,164 @@ exports.job = async (req, res) => {
|
||||
bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
|
||||
)[0];
|
||||
|
||||
const bucketMatrix = {};
|
||||
const yesterday = moment().subtract(1, "day");
|
||||
//Get latest date + add 5 days to allow for back end adding..
|
||||
const load = {
|
||||
productionTotal: {},
|
||||
};
|
||||
//Set the current load.
|
||||
ssbuckets.forEach((bucket) => {
|
||||
load.productionTotal[bucket.id] = { count: 0, label: bucket.label };
|
||||
});
|
||||
|
||||
const totalMatrixDays = moment
|
||||
.max([
|
||||
...appointments.map((a) => moment(a.start)),
|
||||
...jobs
|
||||
.map((p) => moment(p.scheduled_completion))
|
||||
.filter((p) => p.isValid() && p.isAfter(yesterday)),
|
||||
])
|
||||
.add("5", "days")
|
||||
.diff(moment(), "days");
|
||||
const filteredProdJobsList = prodJobs.filter(
|
||||
(j) => JobBucket.id === CheckJobBucket(ssbuckets, j)
|
||||
);
|
||||
|
||||
//Initialize the bucket matrix
|
||||
for (var i = 0; i < totalMatrixDays; i++) {
|
||||
const theDate = moment().add(i, "days").format("yyyy-MM-DD");
|
||||
//Only need to create a matrix for jobs of the same bucket.
|
||||
bucketMatrix[theDate] = { in: 0, out: 0 };
|
||||
|
||||
// ssbuckets.forEach((bucket) => {
|
||||
// bucketMatrix[theDate] = {
|
||||
// ...bucketMatrix[theDate],
|
||||
// [bucket.id]: { in: 0, out: 0 },
|
||||
// };
|
||||
// });
|
||||
}
|
||||
|
||||
//Populate the jobs scheduled to come in.
|
||||
appointments.forEach((appointment) => {
|
||||
if (!appointment.block) {
|
||||
const jobHrs =
|
||||
appointment.job.joblines_aggregate.aggregate.sum.mod_lb_hrs;
|
||||
//Is the job in the same bucket?
|
||||
const appointmentBucket = ssbuckets.filter(
|
||||
(bucket) =>
|
||||
bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
|
||||
)[0];
|
||||
if (appointmentBucket.id === JobBucket.id) {
|
||||
//Theyre the same classification. Add it to the matrix.
|
||||
const appDate = moment(appointment.start).format("yyyy-MM-DD");
|
||||
bucketMatrix[appDate] = {
|
||||
...bucketMatrix[appDate],
|
||||
in: bucketMatrix[appDate].in + 1,
|
||||
};
|
||||
}
|
||||
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 {
|
||||
//remove the date from the possible list.
|
||||
const appDate = moment(appointment.start).format("yyyy-MM-DD");
|
||||
bucketMatrix[appDate] = {
|
||||
...bucketMatrix[appDate],
|
||||
blocked: true,
|
||||
console.log("Uh oh, this job doesn't fit in a bucket!", item);
|
||||
}
|
||||
});
|
||||
|
||||
const filteredArrJobs = arrJobs.filter(
|
||||
(j) => JobBucket.id === CheckJobBucket(ssbuckets, j)
|
||||
);
|
||||
|
||||
filteredArrJobs.forEach((item) => {
|
||||
const itemDate = moment(item.scheduled_in).format("yyyy-MM-DD");
|
||||
if (!!load[itemDate]) {
|
||||
load[itemDate].hoursIn =
|
||||
(load[itemDate].hoursIn || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].jobsIn.push(item);
|
||||
} else {
|
||||
load[itemDate] = {
|
||||
jobsIn: [item],
|
||||
jobsOut: [],
|
||||
hoursIn:
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
//Populate the jobs that are leaving today.
|
||||
const todayIsoString = moment().format("yyyy-MM-DD");
|
||||
jobs.forEach((pjob) => {
|
||||
const jobHrs =
|
||||
pjob.larhrs.aggregate.sum.mod_lb_hrs +
|
||||
pjob.labhrs.aggregate.sum.mod_lb_hrs;
|
||||
//Is the job in the same bucket?
|
||||
const pjobBucket = ssbuckets.filter(
|
||||
(bucket) =>
|
||||
bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
|
||||
)[0];
|
||||
if (pjobBucket.id === JobBucket.id) {
|
||||
//Theyre the same classification. Add it to the matrix.
|
||||
const compDate = moment(pjob.scheduled_completion);
|
||||
//Is the schedule completion behind today? If so, use today as it.
|
||||
let dateToUse;
|
||||
dateToUse = compDate.isValid()
|
||||
? moment().diff(compDate, "days") < 0
|
||||
? compDate.format("yyyy-MM-DD")
|
||||
: todayIsoString
|
||||
: todayIsoString;
|
||||
//Get the completing jobs.
|
||||
let problemJobs = [];
|
||||
const filteredCompJobs = compJobs.filter(
|
||||
(j) => JobBucket.id === CheckJobBucket(ssbuckets, j)
|
||||
);
|
||||
|
||||
bucketMatrix[dateToUse] = {
|
||||
...bucketMatrix[dateToUse],
|
||||
out: (bucketMatrix[dateToUse].out || 0) + 1,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
//Propose the first 5 dates where we are below target.
|
||||
|
||||
const possibleDates = [];
|
||||
const bucketMatrixKeys = Object.keys(bucketMatrix);
|
||||
bucketMatrixKeys.forEach((bmkey) => {
|
||||
const isShopOpen =
|
||||
workingdays[dayOfWeekMapper(moment(bmkey).day())] &&
|
||||
!bucketMatrix[bmkey].blocked;
|
||||
filteredCompJobs.forEach((item) => {
|
||||
const inProdJobs = filteredProdJobsList.find((p) => p.id === item.id);
|
||||
const inArrJobs = filteredArrJobs.find((p) => p.id === item.id);
|
||||
|
||||
if (
|
||||
JobBucket.target > bucketMatrix[bmkey].in - bucketMatrix[bmkey].out &&
|
||||
!(inProdJobs || inArrJobs) &&
|
||||
!moment(item.actual_completion || item.scheduled_completion).isSame(
|
||||
moment(),
|
||||
"day"
|
||||
)
|
||||
) {
|
||||
// NOT FOUND!
|
||||
console.log("PROBLEM JOB", item);
|
||||
problemJobs.push({
|
||||
...item,
|
||||
code: "Job is scheduled for completion, but it is not marked in production nor is it an arriving job in this period. Check the scheduled in and completion dates",
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
const itemDate = moment(
|
||||
item.actual_completion || item.scheduled_completion
|
||||
).format("yyyy-MM-DD");
|
||||
if (!!load[itemDate]) {
|
||||
load[itemDate].hoursOut =
|
||||
(load[itemDate].hoursOut || 0) +
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
load[itemDate].jobsOut.push(item);
|
||||
} else {
|
||||
load[itemDate] = {
|
||||
jobsOut: [item],
|
||||
hoursOut:
|
||||
item.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
item.larhrs.aggregate.sum.mod_lb_hrs,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Propagate the expected load to each day.
|
||||
const yesterday = moment().subtract(1, "day");
|
||||
const today = moment().startOf("day");
|
||||
|
||||
const end = moment.max([
|
||||
...filteredArrJobs.map((a) => moment(a.scheduled_in)),
|
||||
...filteredCompJobs
|
||||
.map((p) => moment(p.actual_completion || p.scheduled_completion))
|
||||
.filter((p) => p.isValid() && p.isAfter(yesterday)),
|
||||
]);
|
||||
const range = Math.round(moment.duration(end.diff(today)).asDays());
|
||||
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,
|
||||
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).format("YYYY-MM-DD");
|
||||
if (load[startIsoFormat]) load[startIsoFormat].blocked = true;
|
||||
else {
|
||||
load[startIsoFormat] = { blocked: true };
|
||||
}
|
||||
});
|
||||
// //Propose the first 5 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;
|
||||
|
||||
if (
|
||||
load[loadKey].expectedLoad &&
|
||||
load[loadKey].expectedLoad[JobBucket.id] &&
|
||||
JobBucket.target > load[loadKey].expectedLoad[JobBucket.id].count &&
|
||||
isShopOpen
|
||||
)
|
||||
possibleDates.push(new Date(bmkey).toISOString().substr(0, 10));
|
||||
possibleDates.push(new Date(loadKey).toISOString().substr(0, 10));
|
||||
});
|
||||
|
||||
if (possibleDates.length < 6) {
|
||||
@@ -147,7 +207,7 @@ exports.job = async (req, res) => {
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("smart-scheduling-error", "ERROR", req.user.email, jobId, {
|
||||
error: JSON.stringify(error),
|
||||
error,
|
||||
});
|
||||
res.status(400).send(error);
|
||||
}
|
||||
@@ -171,3 +231,47 @@ const dayOfWeekMapper = (numberOfDay) => {
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user