Added basic bucketed smart scheduling BOD-4
This commit is contained in:
@@ -4,3 +4,7 @@
|
|||||||
box-sizing: unset !important;
|
box-sizing: unset !important;
|
||||||
min-height: unset !important;
|
min-height: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.imex-event-arrived {
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import moment from "moment";
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Calendar, momentLocalizer } from "react-big-calendar";
|
import { Calendar, momentLocalizer } from "react-big-calendar";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import Event from "../schedule-event/schedule-event.container";
|
import Event from "../schedule-event/schedule-event.container";
|
||||||
import HeaderComponent from "./schedule-calendar-header.component";
|
import HeaderComponent from "./schedule-calendar-header.component";
|
||||||
import "./schedule-calendar.styles.scss";
|
import "./schedule-calendar.styles.scss";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
const localizer = momentLocalizer(moment);
|
const localizer = momentLocalizer(moment);
|
||||||
|
export function ScheduleCalendarWrapperComponent({
|
||||||
export default function ScheduleCalendarWrapperComponent({
|
bodyshop,
|
||||||
data,
|
data,
|
||||||
refetch,
|
refetch,
|
||||||
defaultView,
|
defaultView,
|
||||||
@@ -19,6 +25,21 @@ export default function ScheduleCalendarWrapperComponent({
|
|||||||
}) {
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleEventPropStyles = (event, start, end, isSelected) => {
|
||||||
|
// if (!!!bodyshop.ssbuckets) {
|
||||||
|
// return {};
|
||||||
|
// }
|
||||||
|
// const defaultEventColor = "#3174ad";
|
||||||
|
// const jobHrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs;
|
||||||
|
// const JobBucket = bodyshop.ssbuckets.filter(
|
||||||
|
// (bucket) =>
|
||||||
|
// bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
|
||||||
|
// )[0];
|
||||||
|
|
||||||
|
return { className: event.arrived ? "imex-event-arrived" : "" };
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Calendar
|
<Calendar
|
||||||
events={data}
|
events={data}
|
||||||
@@ -41,6 +62,7 @@ export default function ScheduleCalendarWrapperComponent({
|
|||||||
localizer={localizer}
|
localizer={localizer}
|
||||||
min={new Date("2020-01-01T06:00:00")} //TODO Read from business settings.
|
min={new Date("2020-01-01T06:00:00")} //TODO Read from business settings.
|
||||||
max={new Date("2020-01-01T20:00:00")}
|
max={new Date("2020-01-01T20:00:00")}
|
||||||
|
eventPropGetter={handleEventPropStyles}
|
||||||
components={{
|
components={{
|
||||||
event: (e) => Event({ event: e.event, refetch: refetch }),
|
event: (e) => Event({ event: e.event, refetch: refetch }),
|
||||||
header: HeaderComponent,
|
header: HeaderComponent,
|
||||||
@@ -49,3 +71,5 @@ export default function ScheduleCalendarWrapperComponent({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(ScheduleCalendarWrapperComponent);
|
||||||
|
|||||||
@@ -106,9 +106,9 @@ export function ScheduleEventComponent({
|
|||||||
|
|
||||||
const RegularEvent = event.isintake ? (
|
const RegularEvent = event.isintake ? (
|
||||||
<div>
|
<div>
|
||||||
<strong style={{ color: event.arrived ? "green" : "" }}>{`${
|
<strong>{`${(event.job && event.job.ownr_fn) || ""} ${
|
||||||
(event.job && event.job.ownr_fn) || ""
|
(event.job && event.job.ownr_ln) || ""
|
||||||
} ${(event.job && event.job.ownr_ln) || ""}`}</strong>
|
}`}</strong>
|
||||||
<div style={{ margin: 4 }}>
|
<div style={{ margin: 4 }}>
|
||||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||||
(event.job && event.job.v_make_desc) || ""
|
(event.job && event.job.v_make_desc) || ""
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import axios from "axios";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { auth } from "../../firebase/firebase.utils";
|
import { auth } from "../../firebase/firebase.utils";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import EmailInput from "../form-items-formatted/email-form-item.component";
|
import EmailInput from "../form-items-formatted/email-form-item.component";
|
||||||
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
|
import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container";
|
||||||
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import moment from "moment";
|
||||||
|
|
||||||
export default function ScheduleJobModalComponent({
|
export default function ScheduleJobModalComponent({
|
||||||
existingAppointments,
|
existingAppointments,
|
||||||
@@ -41,7 +42,7 @@ export default function ScheduleJobModalComponent({
|
|||||||
return (
|
return (
|
||||||
<Row gutter={[32, 32]}>
|
<Row gutter={[32, 32]}>
|
||||||
<Col span={14}>
|
<Col span={14}>
|
||||||
<div className='imex-flex-row imex-flex-row__flex-space-around'>
|
<div className="imex-flex-row imex-flex-row__flex-space-around">
|
||||||
<strong>{t("appointments.fields.time")}</strong>
|
<strong>{t("appointments.fields.time")}</strong>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
value={appData.start}
|
value={appData.start}
|
||||||
@@ -53,15 +54,20 @@ export default function ScheduleJobModalComponent({
|
|||||||
{t("appointments.actions.smartscheduling")}
|
{t("appointments.actions.smartscheduling")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className='imex-flex-row imex-flex-row__flex-space-around'>
|
<div className="imex-flex-row imex-flex-row__flex-space-around">
|
||||||
{appData.smartDates.map((d, idx) => (
|
{appData.smartDates.map((d, idx) => (
|
||||||
<Button
|
<Button
|
||||||
className='imex-flex-row__margin'
|
className="imex-flex-row__margin"
|
||||||
key={idx}
|
key={idx}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppData({ ...appData, start: d });
|
console.log("new Date(d)", new Date(d));
|
||||||
}}>
|
setAppData({
|
||||||
<DateTimeFormatter>{d}</DateTimeFormatter>
|
...appData,
|
||||||
|
start: new moment(d).add(8, "hours"),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DateFormatter>{d}</DateFormatter>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +81,8 @@ export default function ScheduleJobModalComponent({
|
|||||||
defaultChecked={appData.notifyCustomer}
|
defaultChecked={appData.notifyCustomer}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setAppData({ ...appData, notifyCustomer: e.target.checked })
|
setAppData({ ...appData, notifyCustomer: e.target.checked })
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
{t("jobs.labels.appointmentconfirmation")}
|
{t("jobs.labels.appointmentconfirmation")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<EmailInput
|
<EmailInput
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function ScheduleJobModalContainer({
|
|||||||
start: null,
|
start: null,
|
||||||
smartDates: [],
|
smartDates: [],
|
||||||
});
|
});
|
||||||
}, [job, , setAppData]);
|
}, [job, setAppData]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -158,7 +158,8 @@ export function ScheduleJobModalContainer({
|
|||||||
okButtonProps={{
|
okButtonProps={{
|
||||||
disabled: appData.start ? false : true,
|
disabled: appData.start ? false : true,
|
||||||
loading: loading,
|
loading: loading,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<ScheduleJobModalComponent
|
<ScheduleJobModalComponent
|
||||||
existingAppointments={existingAppointments}
|
existingAppointments={existingAppointments}
|
||||||
appData={appData}
|
appData={appData}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
accountingconfig
|
accountingconfig
|
||||||
appt_length
|
appt_length
|
||||||
stripe_acct_id
|
stripe_acct_id
|
||||||
|
ssbuckets
|
||||||
employees {
|
employees {
|
||||||
id
|
id
|
||||||
first_name
|
first_name
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ query QUERY_INVOICES_FOR_PAYABLES_EXPORT($invoices: [uuid!]!) {
|
|||||||
|
|
||||||
exports.QUERY_UPCOMING_APPOINTMENTS = `
|
exports.QUERY_UPCOMING_APPOINTMENTS = `
|
||||||
query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
|
query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
|
||||||
jobs_by_pk(id: $jobId){
|
jobs_by_pk(id: $jobId) {
|
||||||
bodyshop{
|
bodyshop {
|
||||||
ssbuckets
|
ssbuckets
|
||||||
}
|
}
|
||||||
jobhrs: joblines_aggregate {
|
jobhrs: joblines_aggregate {
|
||||||
@@ -122,23 +122,27 @@ query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
appointments(where: {start: {_gt: $now}}) {
|
appointments(where: {_and: {canceled: {_eq: false}, start: {_gte: $now}}}) {
|
||||||
start
|
start
|
||||||
isintake
|
isintake
|
||||||
id
|
id
|
||||||
job {
|
job {
|
||||||
joblines_aggregate {
|
joblines_aggregate {
|
||||||
aggregate {
|
aggregate {
|
||||||
sum {
|
sum {
|
||||||
mod_lb_hrs
|
mod_lb_hrs
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
productionview {
|
||||||
|
id
|
||||||
|
labhrs
|
||||||
|
larhrs
|
||||||
|
scheduled_completion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -29,48 +29,103 @@ exports.job = async (req, res) => {
|
|||||||
jobId: jobId,
|
jobId: jobId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { appointments } = result;
|
const { appointments, productionview } = result;
|
||||||
const { ssbuckets } = result.jobs_by_pk.bodyshop;
|
const { ssbuckets } = result.jobs_by_pk.bodyshop;
|
||||||
const jobhrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs;
|
const jobHrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs;
|
||||||
|
|
||||||
const JobClassification = ssbuckets.filter(
|
const JobBucket = ssbuckets.filter(
|
||||||
(b) => b.gte <= jobhrs && b.lt > jobhrs
|
(bucket) =>
|
||||||
);
|
bucket.gte <= jobHrs && (!!bucket.lt ? bucket.lt > jobHrs : true)
|
||||||
//Create a matrix of load bucket
|
)[0];
|
||||||
|
|
||||||
const bucketMatrix = {};
|
const bucketMatrix = {};
|
||||||
|
|
||||||
//Get latest date + add 5 days.
|
//Get latest date + add 5 days to allow for back end adding..
|
||||||
|
|
||||||
const totalMatrixDays = moment
|
const totalMatrixDays = moment
|
||||||
.max(appointments.map((a) => moment(a.start)))
|
.max([
|
||||||
|
...appointments.map((a) => moment(a.start)),
|
||||||
|
...productionview
|
||||||
|
.map((p) => moment(p.scheduled_completion))
|
||||||
|
.filter((p) => p.isValid()),
|
||||||
|
])
|
||||||
.add("5", "days")
|
.add("5", "days")
|
||||||
.diff(moment(), "days");
|
.diff(moment(), "days");
|
||||||
|
|
||||||
for (var i = 0; i++; i < totalMatrixDays) {
|
//Initialize the bucket matrix
|
||||||
|
for (i = 0; i < totalMatrixDays; i++) {
|
||||||
const theDate = moment().add(i, "days").toISOString().substr(0, 10);
|
const theDate = moment().add(i, "days").toISOString().substr(0, 10);
|
||||||
console.log("theDate", theDate);
|
//Only need to create a matrix for jobs of the same bucket.
|
||||||
ssbuckets.forEach((bucket) => {
|
bucketMatrix[theDate] = { in: 0, out: 0 };
|
||||||
bucketMatrix[theDate][bucket.id] = { 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) => {
|
appointments.forEach((appointment) => {
|
||||||
//Get the day of the appointment.
|
const jobHrs =
|
||||||
const appDate = moment(appointment.start).toISOString().substr(0, 10);
|
appointment.job.joblines_aggregate.aggregate.sum.mod_lb_hrs;
|
||||||
!!bucketMatrix[appDate] ? {} : {};
|
//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).toISOString().substr(0, 10);
|
||||||
|
bucketMatrix[appDate] = {
|
||||||
|
...bucketMatrix[appDate],
|
||||||
|
in: bucketMatrix[appDate].in + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//Calculate the load for the shop looking forward.
|
//Populate the jobs that are leaving today.
|
||||||
|
const todayIsoString = moment().toISOString().substr(0, 10);
|
||||||
|
productionview.forEach((pjob) => {
|
||||||
|
const jobHrs = pjob.larhrs + pjob.labhrs;
|
||||||
|
//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.toISOString().substr(0, 10)
|
||||||
|
: todayIsoString
|
||||||
|
: todayIsoString;
|
||||||
|
|
||||||
|
bucketMatrix[dateToUse] = {
|
||||||
|
...bucketMatrix[dateToUse],
|
||||||
|
out: bucketMatrix[dateToUse].out + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//Propose the first 5 dates where we are below target.
|
||||||
|
|
||||||
const possibleDates = [];
|
const possibleDates = [];
|
||||||
|
const bucketMatrixKeys = Object.keys(bucketMatrix);
|
||||||
|
bucketMatrixKeys.forEach((bmkey) => {
|
||||||
|
if (JobBucket.target > bucketMatrix[bmkey].in - bucketMatrix[bmkey].out)
|
||||||
|
possibleDates.push(new Date(bmkey).toISOString().substr(0, 10));
|
||||||
|
});
|
||||||
|
|
||||||
//Temp
|
//Temp
|
||||||
possibleDates.push(new Date());
|
// possibleDates.push(new Date());
|
||||||
possibleDates.push(new Date());
|
// possibleDates.push(new Date());
|
||||||
possibleDates.push(new Date());
|
// possibleDates.push(new Date());
|
||||||
possibleDates.push(new Date());
|
// possibleDates.push(new Date());
|
||||||
possibleDates.push(new Date());
|
// possibleDates.push(new Date());
|
||||||
//Get a list of upcoming appointments
|
//Get a list of upcoming appointments
|
||||||
//Get the config for each day
|
//Get the config for each day
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user