Added basic bucketed smart scheduling BOD-4

This commit is contained in:
Patrick Fic
2020-06-24 11:20:27 -07:00
parent 73ff905b58
commit 997ea6cbcf
8 changed files with 148 additions and 52 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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) || ""

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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
}
}
`; `;

View File

@@ -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