From cb337b557c00ddcb4c38f126eb313620b3532494 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 17 Sep 2020 14:28:48 -0700 Subject: [PATCH] Added predictive completion time based on cycle times for job scheduling. BOD-401 --- bodyshop_translations.babel | 21 +++ .../form-date-picker.component.jsx | 1 + .../form-date-time-picker.component.jsx | 3 +- .../schedule-job-modal.component.jsx | 158 ++++++++++++------ .../schedule-job-modal.container.jsx | 97 ++++++----- .../shop-info/shop-info.component.jsx | 86 +++++++++- client/src/graphql/bodyshop.queries.js | 2 + client/src/graphql/jobs.queries.js | 22 +++ client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + .../down.yaml | 5 + .../up.yaml | 6 + .../down.yaml | 69 ++++++++ .../up.yaml | 70 ++++++++ .../down.yaml | 63 +++++++ .../up.yaml | 64 +++++++ hasura/migrations/metadata.yaml | 2 + server/scheduling/scheduling-job.js | 1 + 19 files changed, 572 insertions(+), 101 deletions(-) create mode 100644 hasura/migrations/1600294031854_alter_table_public_bodyshops_add_column_target_touchtime/down.yaml create mode 100644 hasura/migrations/1600294031854_alter_table_public_bodyshops_add_column_target_touchtime/up.yaml create mode 100644 hasura/migrations/1600294058257_update_permission_user_public_table_bodyshops/down.yaml create mode 100644 hasura/migrations/1600294058257_update_permission_user_public_table_bodyshops/up.yaml create mode 100644 hasura/migrations/1600294065934_update_permission_user_public_table_bodyshops/down.yaml create mode 100644 hasura/migrations/1600294065934_update_permission_user_public_table_bodyshops/up.yaml diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 2f0251d2e..1c380c2fc 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -3873,6 +3873,27 @@ + + target_touchtime + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + zip_post false diff --git a/client/src/components/form-date-picker/form-date-picker.component.jsx b/client/src/components/form-date-picker/form-date-picker.component.jsx index 479cd8bf6..f37b3c6d1 100644 --- a/client/src/components/form-date-picker/form-date-picker.component.jsx +++ b/client/src/components/form-date-picker/form-date-picker.component.jsx @@ -26,6 +26,7 @@ const FormDatePicker = ({ value, onChange, onBlur, ...restProps }, ref) => { value={value ? moment(value) : null} onChange={handleChange} format={dateFormat} + onBlur={onBlur} {...restProps} /> diff --git a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx index 37faa4ad8..826e3a6a3 100644 --- a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx +++ b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx @@ -15,13 +15,14 @@ const DateTimePicker = ({ value, onChange, onBlur }, ref) => { return (
- +
diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx index 7404b81c7..7d45c1ac8 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.component.jsx @@ -1,29 +1,43 @@ -import { Button, Checkbox, Col, Row } from "antd"; +import { Button, Col, Form, Row, Switch } from "antd"; import axios from "axios"; import moment from "moment"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; import { DateFormatter } from "../../utils/DateFormatter"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; import EmailInput from "../form-items-formatted/email-form-item.component"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.container"; import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component"; -export default function ScheduleJobModalComponent({ - jobId, +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export function ScheduleJobModalComponent({ + bodyshop, + form, existingAppointments, - appData, - setAppData, + lbrHrsData, + jobId, }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); + const [smartOptions, setSmartOptions] = useState([]); + const handleAuto = async () => { setLoading(true); try { const response = await axios.post("/scheduling/job", { - jobId: "661dd1d5-bf06-426f-8bd2-bd9e41de8eb1", + jobId: jobId, }); - setAppData({ ...appData, smartDates: response.data }); + if (response.data) setSmartOptions(response.data); } catch (error) { console.log("error", error, error.message); } finally { @@ -34,60 +48,100 @@ export default function ScheduleJobModalComponent({ //TODO Existing appointments list only refreshes sometimes after modal close. May have to do with the container class. return ( - -
- {t("appointments.fields.time")} - { - setAppData({ ...appData, start: e }); - }} - /> + + + + { + const values = form.getFieldsValue(); + if (lbrHrsData) { + const totalHours = + lbrHrsData.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs + + lbrHrsData.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs; + + if (values.start && !values.scheduled_completion) + form.setFieldsValue({ + scheduled_completion: moment(values.start).businessAdd( + totalHours / bodyshop.target_touchtime, + "days" + ), + }); + } + }} + /> + -
-
- {appData.smartDates.map((d, idx) => ( - - ))} -
- +
+ {smartOptions.map((d, idx) => ( + + ))} +
+ + + + + + + + + + + + {t("appointments.labels.history")} - - - setAppData({ ...appData, notifyCustomer: e.target.checked }) - } - > - {t("jobs.labels.appointmentconfirmation")} - - setAppData({ ...appData, email: e.target.value })} - /> - -
- -
+ + + {() => { + const values = form.getFieldsValue(); + + return ( +
+ +
+ ); + }} +
); } +export default connect( + mapStateToProps, + mapDispatchToProps +)(ScheduleJobModalComponent); diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx index ac7557689..c929fcd86 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx @@ -1,23 +1,24 @@ -import React, { useState, useEffect } from "react"; -import ScheduleJobModalComponent from "./schedule-job-modal.component"; import { useMutation, useQuery } from "@apollo/react-hooks"; -import { - INSERT_APPOINTMENT, - CANCEL_APPOINTMENT_BY_ID, - QUERY_APPOINTMENTS_BY_JOBID, -} from "../../graphql/appointments.queries"; -import moment from "moment"; -import { notification, Modal } from "antd"; +//import moment from "moment"; +import { Form, Modal, notification } from "antd"; +import moment from "moment-business-days"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { UPDATE_JOBS } from "../../graphql/jobs.queries"; -import { setEmailOptions } from "../../redux/email/email.actions"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; -import { selectSchedule } from "../../redux/modals/modals.selectors"; -import { toggleModalVisible } from "../../redux/modals/modals.actions"; -import { TemplateList } from "../../utils/TemplateConstants"; import { logImEXEvent } from "../../firebase/firebase.utils"; +import { + CANCEL_APPOINTMENT_BY_ID, + INSERT_APPOINTMENT, + QUERY_APPOINTMENTS_BY_JOBID, +} from "../../graphql/appointments.queries"; +import { QUERY_LBR_HRS_BY_PK, UPDATE_JOBS } from "../../graphql/jobs.queries"; +import { setEmailOptions } from "../../redux/email/email.actions"; +import { toggleModalVisible } from "../../redux/modals/modals.actions"; +import { selectSchedule } from "../../redux/modals/modals.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { TemplateList } from "../../utils/TemplateConstants"; +import ScheduleJobModalComponent from "./schedule-job-modal.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -36,26 +37,23 @@ export function ScheduleJobModalContainer({ }) { const { visible, context, actions } = scheduleModal; const { jobId, job, previousEvent } = context; + const { refetch } = actions; - const [loading, setLoading] = useState(false); - const [appData, setAppData] = useState({ - notifyCustomer: !!(job && job.ownr_ea), - email: (job && job.ownr_ea) || "", - start: null, - smartDates: [], + const [form] = Form.useForm(); + + const { data: lbrHrsData } = useQuery(QUERY_LBR_HRS_BY_PK, { + variables: { id: job && job.id }, + skip: !job || !job.id, }); + + const [loading, setLoading] = useState(false); const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID); const [insertAppointment] = useMutation(INSERT_APPOINTMENT); const [updateJobStatus] = useMutation(UPDATE_JOBS); useEffect(() => { - setAppData({ - notifyCustomer: !!(job && job.ownr_ea), - email: (job && job.ownr_ea) || "", - start: null, - smartDates: [], - }); - }, [job, setAppData]); + form.resetFields(); + }, [job, form]); const { t } = useTranslation(); @@ -65,7 +63,7 @@ export function ScheduleJobModalContainer({ skip: !visible || !!!jobId, }); - const handleOk = async () => { + const handleFinish = async (values) => { logImEXEvent("schedule_new_appointment"); setLoading(true); @@ -90,11 +88,10 @@ export function ScheduleJobModalContainer({ const appt = await insertAppointment({ variables: { app: { - //...appData, jobid: jobId, bodyshopid: bodyshop.id, - start: moment(appData.start), - end: moment(appData.start).add(bodyshop.appt_length || 60, "minutes"), + start: moment(values.start), + end: moment(values.start).add(bodyshop.appt_length || 60, "minutes"), }, }, }); @@ -117,7 +114,8 @@ export function ScheduleJobModalContainer({ fields: { status: bodyshop.md_ro_statuses.default_scheduled, date_scheduled: new Date(), - scheduled_in: appData.start, + scheduled_in: values.start, + scheduled_completion: values.scheduled_completion, }, }, }); @@ -133,10 +131,10 @@ export function ScheduleJobModalContainer({ } setLoading(false); toggleModalVisible(); - if (appData.notifyCustomer) { + if (values.notifyCustomer) { setEmailOptions({ messageOptions: { - to: [appData.email], + to: [values.email], replyTo: bodyshop.email, }, template: { @@ -154,21 +152,34 @@ export function ScheduleJobModalContainer({ toggleModalVisible()} - onOk={handleOk} + onOk={() => form.submit()} width={"90%"} maskClosable={false} destroyOnClose + forceRender okButtonProps={{ - disabled: appData.start ? false : true, loading: loading, }} > - +
+ +
); } diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index ab0112eca..d2a0c8471 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -27,25 +27,65 @@ export default function ShopInfoComponent({ form, saveLoading }) { const { t } = useTranslation(); return (
- - + - + - + - + @@ -65,6 +105,12 @@ export default function ShopInfoComponent({ form, saveLoading }) { @@ -91,18 +137,36 @@ export default function ShopInfoComponent({ form, saveLoading }) { @@ -467,6 +531,18 @@ export default function ShopInfoComponent({ form, saveLoading }) { > + + + { try { const BearerToken = req.headers.authorization; const { jobId } = req.body; + console.log("exports.job -> jobId", jobId) const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { headers: {