Cleanup on schedule modal + begin smart scheduling BOD-4

This commit is contained in:
Patrick Fic
2020-06-24 08:02:48 -07:00
parent cab080f2de
commit 73ff905b58
11 changed files with 216 additions and 40 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.6.1"> <babeledit_project be_version="2.6.1" version="1.2">
<!-- <!--
BabelEdit project file BabelEdit project file

View File

@@ -1,23 +1,23 @@
import { Button, Checkbox, Col, Row } from "antd"; import { Button, Checkbox, Col, Row } from "antd";
import axios from "axios"; import axios from "axios";
import React 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 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";
export default function ScheduleJobModalComponent({ export default function ScheduleJobModalComponent({
existingAppointments, existingAppointments,
appData, appData,
setAppData, setAppData,
formData,
setFormData,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleAuto = async () => { const handleAuto = async () => {
setLoading(true);
try { try {
const response = await axios.post( const response = await axios.post(
"/scheduling/job", "/scheduling/job",
@@ -29,16 +29,19 @@ export default function ScheduleJobModalComponent({
} }
); );
console.log("response", response); console.log("response", response);
setAppData({ ...appData, smartDates: response.data });
} catch (error) { } catch (error) {
console.log("error", error, error.message); console.log("error", error, error.message);
} finally {
setLoading(false);
} }
}; };
//TODO Existing appointments list only refreshes sometimes after modal close. May have to do with the container class. //TODO Existing appointments list only refreshes sometimes after modal close. May have to do with the container class.
return ( return (
<Row> <Row gutter={[32, 32]}>
<Col span={14}> <Col span={14}>
<div style={{ display: "flex", alignContent: "middle" }}> <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}
@@ -46,10 +49,22 @@ export default function ScheduleJobModalComponent({
setAppData({ ...appData, start: e }); setAppData({ ...appData, start: e });
}} }}
/> />
<Button onClick={handleAuto}> <Button onClick={handleAuto} loading={loading}>
{t("appointments.actions.smartscheduling")} {t("appointments.actions.smartscheduling")}
</Button> </Button>
</div> </div>
<div className='imex-flex-row imex-flex-row__flex-space-around'>
{appData.smartDates.map((d, idx) => (
<Button
className='imex-flex-row__margin'
key={idx}
onClick={() => {
setAppData({ ...appData, start: d });
}}>
<DateTimeFormatter>{d}</DateTimeFormatter>
</Button>
))}
</div>
{t("appointments.labels.history")} {t("appointments.labels.history")}
<ScheduleExistingAppointmentsList <ScheduleExistingAppointmentsList
@@ -57,17 +72,16 @@ export default function ScheduleJobModalComponent({
/> />
<Checkbox <Checkbox
defaultChecked={formData.notifyCustomer} defaultChecked={appData.notifyCustomer}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, notifyCustomer: e.target.checked }) setAppData({ ...appData, notifyCustomer: e.target.checked })
} }>
>
{t("jobs.labels.appointmentconfirmation")} {t("jobs.labels.appointmentconfirmation")}
</Checkbox> </Checkbox>
<EmailInput <EmailInput
defaultValue={formData.email} defaultValue={appData.email}
title={t("owner.fields.ownr_ea")} title={t("owner.fields.ownr_ea")}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) => setAppData({ ...appData, email: e.target.value })}
/> />
</Col> </Col>
<Col span={10}> <Col span={10}>

View File

@@ -35,28 +35,25 @@ export function ScheduleJobModalContainer({
const { visible, context, actions } = scheduleModal; const { visible, context, actions } = scheduleModal;
const { jobId, job, previousEvent } = context; const { jobId, job, previousEvent } = context;
const { refetch } = actions; const { refetch } = actions;
const [loading, setLoading] = useState(false);
const [appData, setAppData] = useState({ const [appData, setAppData] = useState({
notifyCustomer: !!(job && job.ownr_ea),
email: (job && job.ownr_ea) || "",
start: null, start: null,
smartDates: [],
}); });
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID); const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
const [insertAppointment] = useMutation(INSERT_APPOINTMENT); const [insertAppointment] = useMutation(INSERT_APPOINTMENT);
const [updateJobStatus] = useMutation(UPDATE_JOBS); const [updateJobStatus] = useMutation(UPDATE_JOBS);
const [formData, setFormData] = useState({
notifyCustomer: false,
email: (job && job.ownr_ea) || "",
});
useEffect(() => { useEffect(() => {
setFormData({ setAppData({
notifyCustomer: !!(job && job.ownr_ea), notifyCustomer: !!(job && job.ownr_ea),
email: (job && job.ownr_ea) || "", email: (job && job.ownr_ea) || "",
start: null, start: null,
smartDates: [],
}); });
setAppData({ }, [job, , setAppData]);
start: null,
});
}, [job, setFormData, setAppData]);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -68,6 +65,7 @@ export function ScheduleJobModalContainer({
//TODO Customize the amount of minutes it will add. //TODO Customize the amount of minutes it will add.
const handleOk = async () => { const handleOk = async () => {
setLoading(true);
if (!!previousEvent) { if (!!previousEvent) {
const cancelAppt = await cancelAppointment({ const cancelAppt = await cancelAppointment({
variables: { appid: previousEvent }, variables: { appid: previousEvent },
@@ -89,9 +87,10 @@ export function ScheduleJobModalContainer({
const appt = await insertAppointment({ const appt = await insertAppointment({
variables: { variables: {
app: { app: {
...appData, //...appData,
jobid: jobId, jobid: jobId,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
start: moment(appData.start),
end: moment(appData.start).add(bodyshop.appt_length || 60, "minutes"), end: moment(appData.start).add(bodyshop.appt_length || 60, "minutes"),
}, },
}, },
@@ -129,12 +128,12 @@ export function ScheduleJobModalContainer({
return; return;
} }
} }
setLoading(false);
toggleModalVisible(); toggleModalVisible();
if (formData.notifyCustomer) { if (appData.notifyCustomer) {
setEmailOptions({ setEmailOptions({
messageOptions: { messageOptions: {
to: formData.email, to: appData.email,
replyTo: bodyshop.email, replyTo: bodyshop.email,
}, },
template: { template: {
@@ -156,14 +155,14 @@ export function ScheduleJobModalContainer({
width={"90%"} width={"90%"}
maskClosable={false} maskClosable={false}
destroyOnClose destroyOnClose
okButtonProps={{ disabled: appData.start ? false : true }} okButtonProps={{
> disabled: appData.start ? false : true,
loading: loading,
}}>
<ScheduleJobModalComponent <ScheduleJobModalComponent
existingAppointments={existingAppointments} existingAppointments={existingAppointments}
appData={appData} appData={appData}
setAppData={setAppData} setAppData={setAppData}
formData={formData}
setFormData={setFormData}
/> />
</Modal> </Modal>
); );

View File

@@ -106,10 +106,5 @@ export function* calculateScheduleLoad({ payload: end }) {
} }
export function* applicationSagas() { export function* applicationSagas() {
yield all([ yield all([call(onCalculateScheduleLoad)]);
call(onCalculateScheduleLoad),
// call(onSendEmailFailure),
// call(onSendEmailSuccess)
//call(onRenderTemplate),
]);
} }

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "ssbuckets";
type: run_sql

View File

@@ -0,0 +1,6 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "ssbuckets" jsonb NULL DEFAULT
jsonb_build_array();
type: run_sql

View File

@@ -0,0 +1,53 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accountingconfig
- address1
- address2
- appt_length
- city
- country
- created_at
- email
- federal_tax_id
- id
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- invoice_tax_rates
- logo_img_path
- md_order_statuses
- md_responsibility_centers
- md_ro_statuses
- messagingservicesid
- production_config
- region_config
- shopname
- shoprates
- state
- state_tax_id
- stripe_acct_id
- template_header
- textid
- updated_at
- zip_post
computed_fields: []
filter:
associations:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -0,0 +1,54 @@
- args:
role: user
table:
name: bodyshops
schema: public
type: drop_select_permission
- args:
permission:
allow_aggregations: false
columns:
- accountingconfig
- address1
- address2
- appt_length
- city
- country
- created_at
- email
- federal_tax_id
- id
- inhousevendorid
- insurance_vendor_id
- intakechecklist
- invoice_tax_rates
- logo_img_path
- md_order_statuses
- md_responsibility_centers
- md_ro_statuses
- messagingservicesid
- production_config
- region_config
- shopname
- shoprates
- ssbuckets
- state
- state_tax_id
- stripe_acct_id
- template_header
- textid
- updated_at
- zip_post
computed_fields: []
filter:
associations:
bodyshop:
associations:
user:
authid:
_eq: X-Hasura-User-Id
role: user
table:
name: bodyshops
schema: public
type: create_select_permission

View File

@@ -471,6 +471,7 @@ tables:
- region_config - region_config
- shopname - shopname
- shoprates - shoprates
- ssbuckets
- state - state
- state_tax_id - state_tax_id
- stripe_acct_id - stripe_acct_id

View File

@@ -110,7 +110,20 @@ query QUERY_INVOICES_FOR_PAYABLES_EXPORT($invoices: [uuid!]!) {
`; `;
exports.QUERY_UPCOMING_APPOINTMENTS = ` exports.QUERY_UPCOMING_APPOINTMENTS = `
query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!) { query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) {
jobs_by_pk(id: $jobId){
bodyshop{
ssbuckets
}
jobhrs: joblines_aggregate {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
appointments(where: {start: {_gt: $now}}) { appointments(where: {start: {_gt: $now}}) {
start start
isintake isintake
@@ -127,4 +140,5 @@ exports.QUERY_UPCOMING_APPOINTMENTS = `
} }
} }
`; `;

View File

@@ -2,6 +2,7 @@ const GraphQLClient = require("graphql-request").GraphQLClient;
const path = require("path"); const path = require("path");
const queries = require("../graphql-client/queries"); const queries = require("../graphql-client/queries");
const Dinero = require("dinero.js"); const Dinero = require("dinero.js");
const moment = require("moment");
require("dotenv").config({ require("dotenv").config({
path: path.resolve( path: path.resolve(
@@ -14,7 +15,6 @@ exports.job = async (req, res) => {
try { try {
const BearerToken = req.headers.authorization; const BearerToken = req.headers.authorization;
const { jobId } = req.body; const { jobId } = req.body;
console.log("exports.job -> jobId", jobId);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: { headers: {
@@ -26,8 +26,43 @@ exports.job = async (req, res) => {
.setHeaders({ Authorization: BearerToken }) .setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_UPCOMING_APPOINTMENTS, { .request(queries.QUERY_UPCOMING_APPOINTMENTS, {
now: new Date(), now: new Date(),
jobId: jobId,
}); });
const { appointments } = result;
const { ssbuckets } = result.jobs_by_pk.bodyshop;
const jobhrs = result.jobs_by_pk.jobhrs.aggregate.sum.mod_lb_hrs;
const JobClassification = ssbuckets.filter(
(b) => b.gte <= jobhrs && b.lt > jobhrs
);
//Create a matrix of load bucket
const bucketMatrix = {};
//Get latest date + add 5 days.
const totalMatrixDays = moment
.max(appointments.map((a) => moment(a.start)))
.add("5", "days")
.diff(moment(), "days");
for (var i = 0; i++; i < totalMatrixDays) {
const theDate = moment().add(i, "days").toISOString().substr(0, 10);
console.log("theDate", theDate);
ssbuckets.forEach((bucket) => {
bucketMatrix[theDate][bucket.id] = { in: 0, out: 0 };
});
}
appointments.forEach((appointment) => {
//Get the day of the appointment.
const appDate = moment(appointment.start).toISOString().substr(0, 10);
!!bucketMatrix[appDate] ? {} : {};
});
//Calculate the load for the shop looking forward.
const possibleDates = []; const possibleDates = [];
//Temp //Temp