Cleanup on schedule modal + begin smart scheduling BOD-4
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- args:
|
||||||
|
cascade: false
|
||||||
|
read_only: false
|
||||||
|
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "ssbuckets";
|
||||||
|
type: run_sql
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user