From e606401e7682233c8156b282be0da04840d2d061 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 3 Jun 2020 16:17:39 -0700 Subject: [PATCH] Added length of appointment to config + fixed appointments not showing in scheduling modal + added appointment confirmation template. BOD-141 BOD-149 BOD-148 --- bodyshop_translations.babel | 84 +++++++++++++++ .../email-overlay/email-overlay.component.jsx | 11 +- .../email-overlay/email-overlay.container.jsx | 38 +++---- .../email-form-item.component.jsx | 5 +- .../jobs-detail-header.component.jsx | 59 ++++++----- .../scheduler-calendar-wrapper.component.jsx | 5 +- .../schedule-day-view.component.jsx | 4 +- .../schedule-day-view.container.jsx | 16 +-- .../schedule-job-modal.component.jsx | 45 ++++---- .../schedule-job-modal.container.jsx | 100 ++++++++++++------ .../shop-info/shop-info.component.jsx | 85 ++++++++++----- client/src/graphql/bodyshop.queries.js | 1 + client/src/pages/csi/csi.container.page.jsx | 38 ++++--- client/src/translations/en_us/common.json | 4 + client/src/translations/es/common.json | 4 + client/src/translations/fr/common.json | 4 + client/src/utils/TemplateConstants.js | 7 ++ .../appointment_confirmation.query.gql | 11 ++ .../appointment_confirmation.template.html | 9 ++ .../down.yaml | 5 + .../up.yaml | 6 ++ .../down.yaml | 51 +++++++++ .../up.yaml | 52 +++++++++ .../down.yaml | 49 +++++++++ .../up.yaml | 50 +++++++++ hasura/migrations/metadata.yaml | 2 + package.json | 2 + server/render/renderHandlebars.js | 62 ++++++++++- yarn.lock | 5 + 29 files changed, 652 insertions(+), 162 deletions(-) create mode 100644 client/templates/appointment_confirmation/appointment_confirmation.query.gql create mode 100644 client/templates/appointment_confirmation/appointment_confirmation.template.html create mode 100644 hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/down.yaml create mode 100644 hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/up.yaml create mode 100644 hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/down.yaml create mode 100644 hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/up.yaml create mode 100644 hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/down.yaml create mode 100644 hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/up.yaml diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 98a6e9492..ec25175ea 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -280,6 +280,27 @@ + + smartscheduling + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + viewjob false @@ -353,6 +374,27 @@ fields + + time + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + title false @@ -421,6 +463,27 @@ + + history + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + nodateselected false @@ -875,6 +938,27 @@ + + appt_length + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + city false diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index 7cf5f393a..5481737df 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -8,20 +8,21 @@ export default function EmailOverlayComponent({ }) { return (
+ To: - CC + CC: - Subject + Subject: diff --git a/client/src/components/email-overlay/email-overlay.container.jsx b/client/src/components/email-overlay/email-overlay.container.jsx index c6c5ed34b..fefecd593 100644 --- a/client/src/components/email-overlay/email-overlay.container.jsx +++ b/client/src/components/email-overlay/email-overlay.container.jsx @@ -33,6 +33,7 @@ export function EmailOverlayContainer({ }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); + const [sending, setSending] = useState(false); const defaultEmailFrom = { from: { name: bodyshop.shopname || EmailSettings.fromNameDefault, @@ -45,21 +46,19 @@ export function EmailOverlayContainer({ html: "", }); - const handleOk = () => { - //sendEmail(messageOptions); - axios - .post("/sendemail", messageOptions) - .then((response) => { - console.log(JSON.stringify(response)); - notification["success"]({ message: t("emails.successes.sent") }); - toggleEmailOverlayVisible(); - }) - .catch((error) => { - console.log(JSON.stringify(error)); - notification["error"]({ - message: t("emails.errors.notsent", { message: error.message }), - }); + const handleOk = async () => { + setSending(true); + try { + const emailResponse = await axios.post("/sendemail", messageOptions); + notification["success"]({ message: t("emails.successes.sent") }); + toggleEmailOverlayVisible(); + } catch (error) { + console.log(JSON.stringify(error)); + notification["error"]({ + message: t("emails.errors.notsent", { message: error.message }), }); + } + setSending(false); }; const handleConfigChange = (event) => { @@ -72,6 +71,7 @@ export function EmailOverlayContainer({ const render = async () => { setLoading(true); + console.log("emailConfig", emailConfig); let html = await RenderTemplate(emailConfig.template, bodyshop); setMessageOptions({ ...emailConfig.messageOptions, @@ -93,7 +93,9 @@ export function EmailOverlayContainer({ onOk={handleOk} onCancel={() => { toggleEmailOverlayVisible(); - }}> + }} + okButtonProps={{ loading: sending }} + > diff --git a/client/src/components/form-items-formatted/email-form-item.component.jsx b/client/src/components/form-items-formatted/email-form-item.component.jsx index f3d211a1a..82eb0a1f4 100644 --- a/client/src/components/form-items-formatted/email-form-item.component.jsx +++ b/client/src/components/form-items-formatted/email-form-item.component.jsx @@ -1,13 +1,14 @@ import { Input } from "antd"; import { MailFilled } from "@ant-design/icons"; import React, { forwardRef } from "react"; +import { Link } from "react-router-dom"; function FormItemEmail(props, ref) { return ( + props.defaultValue ? ( + ) : ( diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx index fb6f814c7..1013d0afa 100644 --- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx +++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx @@ -46,7 +46,7 @@ export function JobsDetailHeader({ const tombstoneTitle = (
- + {job.ro_number ? `${t("jobs.fields.ro_number")} ${job.ro_number}` : `EST-${job.est_number}`} @@ -57,7 +57,8 @@ export function JobsDetailHeader({ { updateJobStatus(e.key); - }}> + }} + > {bodyshop.md_ro_statuses.statuses.map((item) => ( {item} ))} @@ -65,12 +66,12 @@ export function JobsDetailHeader({ ); const menuExtra = [ - + , - + , , - , - , ]; @@ -115,48 +119,53 @@ export function JobsDetailHeader({ title={tombstoneTitle} //subTitle={tombstoneSubtitle} tags={ - - {job.status ? {job.status} : null} + + {job.status ? {job.status} : null} {job.inproduction ? ( - {t("jobs.labels.inproduction")} + {t("jobs.labels.inproduction")} ) : null} } - extra={menuExtra}> - - + extra={menuExtra} + > + + {job.clm_total} + key="custowing" + label={t("jobs.fields.customerowing")} + > {job.owner_owing} + key="scp" + label={t("jobs.fields.specialcoveragepolicy")} + > + key="sched_comp" + label={t("jobs.fields.scheduled_completion")} + > {job.scheduled_completion ? ( - {job.scheduled_completion} + {job.scheduled_completion} ) : null} - + {job.cccontracts && job.cccontracts.map((item) => ( + to={`/manage/courtesycars/contracts/${item.id}`} + >
{`${item.agreementnumber} - ${item.start} - ${item.scheduledreturn}`}
))} diff --git a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx index 91042b69d..5c42ae3b6 100644 --- a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx +++ b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx @@ -15,16 +15,16 @@ export default function ScheduleCalendarWrapperComponent({ refetch, defaultView, setDateRangeCallback, + date, ...otherProps }) { const search = queryString.parse(useLocation().search); const history = useHistory(); - return ( { search.date = date.toISOString().substr(0, 10); history.push({ search: queryString.stringify(search) }); @@ -45,6 +45,7 @@ export default function ScheduleCalendarWrapperComponent({ components={{ event: (e) => Event({ event: e.event, refetch: refetch }), header: HeaderComponent, + toolbar: null, }} {...otherProps} /> diff --git a/client/src/components/schedule-day-view/schedule-day-view.component.jsx b/client/src/components/schedule-day-view/schedule-day-view.component.jsx index dd657fcdf..a70ebd87a 100644 --- a/client/src/components/schedule-day-view/schedule-day-view.component.jsx +++ b/client/src/components/schedule-day-view/schedule-day-view.component.jsx @@ -6,15 +6,13 @@ import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/sched export default function ScheduleDayViewComponent({ data, day }) { const { t } = useTranslation(); if (data) - //TODO Remove addtional calendar elements from day view. return ( console.log("e", e)} + date={day} /> ); else return
{t("appointments.labels.nodateselected")}
; diff --git a/client/src/components/schedule-day-view/schedule-day-view.container.jsx b/client/src/components/schedule-day-view/schedule-day-view.container.jsx index 2b0f7ee62..340adca80 100644 --- a/client/src/components/schedule-day-view/schedule-day-view.container.jsx +++ b/client/src/components/schedule-day-view/schedule-day-view.container.jsx @@ -4,22 +4,24 @@ import { useQuery } from "@apollo/react-hooks"; import { QUERY_APPOINTMENT_BY_DATE } from "../../graphql/appointments.queries"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import moment from "moment"; - +import { useTranslation } from "react-i18next"; export default function ScheduleDayViewContainer({ day }) { const { loading, error, data } = useQuery(QUERY_APPOINTMENT_BY_DATE, { variables: { start: moment(day).startOf("day"), - end: moment(day).endOf("day") + end: moment(day).endOf("day"), }, - skip: !day, - fetchPolicy: "network-only" + skip: !!!day, + fetchPolicy: "network-only", }); - + const { t } = useTranslation(); + if (!!!day) return
{t("appointments.labels.nodateselected")}
; if (loading) return ; if (error) return
{error.message}
; let normalizedData; + if (data) { - normalizedData = data.appointments.map(e => { + normalizedData = data.appointments.map((e) => { //Required becuase Hasura returns a string instead of a date object. return Object.assign( {}, @@ -31,6 +33,6 @@ export default function ScheduleDayViewContainer({ day }) { } 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 544eeacbf..fa1b89170 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,4 +1,4 @@ -import { Checkbox, Col, Row, Tabs } from "antd"; +import { Checkbox, Col, Row, Input, Button } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component"; @@ -6,6 +6,7 @@ import ScheduleDayViewContainer from "../schedule-day-view/schedule-day-view.con import ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component"; import axios from "axios"; import { auth } from "../../firebase/firebase.utils"; +import EmailInput from "../form-items-formatted/email-form-item.component"; export default function ScheduleJobModalComponent({ existingAppointments, @@ -37,31 +38,24 @@ export default function ScheduleJobModalComponent({ return ( - - - Automatic Job Selection. - - - - - Manual Job Selection Scheduled Time -
- { - setAppData({ ...appData, start: e }); - }} - /> -
-
-
-
+
+ {t("appointments.fields.time")} + { + setAppData({ ...appData, start: e }); + }} + /> + +
+ + {t("appointments.labels.history")} - { - //TODO Build out notifications. - } + @@ -70,6 +64,11 @@ export default function ScheduleJobModalComponent({ > {t("jobs.labels.appointmentconfirmation")} + setFormData({ ...formData, email: e.target.value })} + /> 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 570a60c3a..cab977d4e 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,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import ScheduleJobModalComponent from "./schedule-job-modal.component"; import { useMutation, useQuery } from "@apollo/react-hooks"; import { @@ -9,12 +9,13 @@ import moment from "moment"; import { notification, Modal } from "antd"; 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"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -22,22 +23,37 @@ const mapStateToProps = createStructuredSelector({ }); const mapDispatchToProps = (dispatch) => ({ toggleModalVisible: () => dispatch(toggleModalVisible("schedule")), + setEmailOptions: (e) => dispatch(setEmailOptions(e)), }); export function ScheduleJobModalContainer({ scheduleModal, bodyshop, toggleModalVisible, + setEmailOptions, }) { const { visible, context, actions } = scheduleModal; - const { jobId } = context; + const { jobId, job } = context; const { refetch } = actions; const [appData, setAppData] = useState({ start: null, }); + const [insertAppointment] = useMutation(INSERT_APPOINTMENT); const [updateJobStatus] = useMutation(UPDATE_JOBS); - const [formData, setFormData] = useState({ notifyCustomer: false }); + const [formData, setFormData] = useState({ + notifyCustomer: false, + email: (job && job.ownr_ea) || "", + }); + + useEffect(() => { + setFormData({ + notifyCustomer: !!(job && job.ownr_ea), + email: (job && job.ownr_ea) || "", + start: null, + }); + }, [job, setFormData]); + const { t } = useTranslation(); const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, { @@ -47,47 +63,67 @@ export function ScheduleJobModalContainer({ }); //TODO Customize the amount of minutes it will add. - const handleOk = () => { - insertAppointment({ + const handleOk = async () => { + const appt = await insertAppointment({ variables: { app: { ...appData, jobid: jobId, bodyshopid: bodyshop.id, - end: moment(appData.start).add(60, "minutes"), + end: moment(appData.start).add(bodyshop.appt_length || 60, "minutes"), }, }, - }) - .then((r) => { - updateJobStatus({ - variables: { - jobIds: [jobId], - fields: { - status: bodyshop.md_ro_statuses.default_scheduled, - date_scheduled: new Date(), - scheduled_in: appData.start, - }, - }, - }).then((r) => { - notification["success"]({ - message: t("appointments.successes.created"), - }); + }); - if (formData.notifyCustomer) { - //TODO Implement customer reminder on scheduling. - alert("Chosed to notify the customer somehow!"); - } - toggleModalVisible(); - if (refetch) refetch(); - }); - }) - .catch((error) => { + if (!!appt.errors) { + notification["error"]({ + message: t("appointments.errors.saving", { + message: JSON.stringify(appt.errors), + }), + }); + return; + } + notification["success"]({ + message: t("appointments.successes.created"), + }); + if (jobId) { + const jobUpdate = await updateJobStatus({ + variables: { + jobIds: [jobId], + fields: { + status: bodyshop.md_ro_statuses.default_scheduled, + date_scheduled: new Date(), + scheduled_in: appData.start, + }, + }, + }); + + if (!!jobUpdate.errors) { notification["error"]({ message: t("appointments.errors.saving", { - message: error.message, + message: JSON.stringify(jobUpdate.errors), }), }); + return; + } + } + + toggleModalVisible(); + if (formData.notifyCustomer) { + setEmailOptions({ + messageOptions: { + to: formData.email, + replyTo: bodyshop.email, + }, + template: { + name: TemplateList.appointment_confirmation.key, + variables: { + id: appt.data.insert_appointments.returning[0].id, + }, + }, }); + } + if (refetch) refetch(); }; return ( diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index c05a7bf2c..e2346cba4 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -9,71 +9,92 @@ export default function ShopInfoComponent({ form }) { const { t } = useTranslation(); return (
- - - - + + + - + - + - + - + - + - + - + + name="federal_tax_id" + > + name="insurance_vendor_id" + > + name="logo_img_path" + > + name="state_tax_id" + > + name={["invoice_tax_rates", "federal_tax_rate"]} + > + name={["invoice_tax_rates", "state_tax_rate"]} + > + name={["invoice_tax_rates", "local_tax_rate"]} + > + + + + + + name={["accountingconfig", "tiers"]} + > 2 3 @@ -101,13 +123,15 @@ export default function ShopInfoComponent({ form }) { message: t("general.validation.required"), }, ]} - name={["accountingconfig", "twotierpref"]}> + name={["accountingconfig", "twotierpref"]} + > - {t("bodyshop.labels.2tiername")} - + } + > + {t("bodyshop.labels.2tiername")} + {t("bodyshop.labels.2tiersource")} @@ -117,18 +141,21 @@ export default function ShopInfoComponent({ form }) { + key="roStatus" + header={t("bodyshop.labels.jobstatuses")} + > + key="orderStatus" + header={t("bodyshop.labels.orderstatuses")} + > + key="responsibilityCenters" + header={t("bodyshop.labels.responsibilitycenters.title")} + > diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 6e5f45310..25521b041 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -78,6 +78,7 @@ export const UPDATE_SHOP = gql` textid production_config invoice_tax_rates + appt_length employees { id first_name diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx index 97eabe566..1cf3e77c8 100644 --- a/client/src/pages/csi/csi.container.page.jsx +++ b/client/src/pages/csi/csi.container.page.jsx @@ -28,9 +28,10 @@ export default function CsiContainerPage() { return (
+ subTitle={t("csi.errors.notfoundsubtitle")} + > {error ? (
ERROR: {error.graphQLErrors.map((e) => e.message)}
) : null} @@ -70,18 +71,20 @@ export default function CsiContainerPage() { return ( + style={{ height: "100vh", display: "flex", flexDirection: "column" }} + >
+ }} + >
{bodyshop.logo_img_path ? ( - Logo + Logo ) : null} -
+
{bodyshop.shopname || ""}
{`${bodyshop.address1 || ""}`}
{`${bodyshop.address2 || ""}`}
@@ -93,13 +96,15 @@ export default function CsiContainerPage() { {t("csi.labels.title")} {`Hi ${job.ownr_fn || ""}!`} - At {bodyshop.shopname || ""}, we value your feedback. We would love to - hear what you have to say. Please fill out the form below. + {`At ${ + bodyshop.shopname || "" + }, we value your feedback. We would love to + hear what you have to say. Please fill out the form below.`}
{submitting.error ? ( - + ) : null} {submitting.submitted ? ( @@ -109,9 +114,10 @@ export default function CsiContainerPage() { margin: "2em 4em", padding: "2em", overflowY: "auto", - }}> + }} + > @@ -123,13 +129,15 @@ export default function CsiContainerPage() { margin: "2em 4em", padding: "2em", overflowY: "auto", - }}> + }} + >
@@ -137,7 +145,7 @@ export default function CsiContainerPage() { )} - Copyright ImEX.Online. Survey ID: {surveyId} + {`Copyright ImEX.Online. Survey ID: ${surveyId}`} ); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 0226308ce..409e56fed 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -23,6 +23,7 @@ "intake": "Intake", "new": "New Appointment", "reschedule": "Reschedule", + "smartscheduling": "SMART Scheduling", "viewjob": "View Job" }, "errors": { @@ -30,11 +31,13 @@ "saving": "Error scheduling appointment. {{message}}" }, "fields": { + "time": "Appointment Time", "title": "Title" }, "labels": { "arrivedon": "Arrived on: ", "cancelledappointment": "Canceled appointment for: ", + "history": "History", "nodateselected": "No date has been selected.", "priorappointments": "Previous Appointments", "scheduledfor": "Scheduled appointment for: " @@ -76,6 +79,7 @@ "fields": { "address1": "Address 1", "address2": "Address 2", + "appt_length": "Default Appointment Length", "city": "City", "country": "Country", "email": "General Shop Email", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 8ad37cc7d..f7e648178 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -23,6 +23,7 @@ "intake": "Consumo", "new": "Nueva cita", "reschedule": "Reprogramar", + "smartscheduling": "", "viewjob": "Ver trabajo" }, "errors": { @@ -30,11 +31,13 @@ "saving": "Error al programar la cita. {{message}}" }, "fields": { + "time": "", "title": "Título" }, "labels": { "arrivedon": "Llegado el:", "cancelledappointment": "Cita cancelada para:", + "history": "", "nodateselected": "No se ha seleccionado ninguna fecha.", "priorappointments": "Nombramientos previos", "scheduledfor": "Cita programada para:" @@ -76,6 +79,7 @@ "fields": { "address1": "", "address2": "", + "appt_length": "", "city": "", "country": "", "email": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index b5f824dab..e64dc2d5c 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -23,6 +23,7 @@ "intake": "Admission", "new": "Nouveau rendez-vous", "reschedule": "Replanifier", + "smartscheduling": "", "viewjob": "Voir le travail" }, "errors": { @@ -30,11 +31,13 @@ "saving": "Erreur lors de la planification du rendez-vous. {{message}}" }, "fields": { + "time": "", "title": "Titre" }, "labels": { "arrivedon": "Arrivé le:", "cancelledappointment": "Rendez-vous annulé pour:", + "history": "", "nodateselected": "Aucune date n'a été sélectionnée.", "priorappointments": "Rendez-vous précédents", "scheduledfor": "Rendez-vous prévu pour:" @@ -76,6 +79,7 @@ "fields": { "address1": "", "address2": "", + "appt_length": "", "city": "", "country": "", "email": "", diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 0bc5aa69c..449895334 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -10,6 +10,13 @@ export const TemplateList = { drivingId: "Appointment Id", key: "appointment_reminder", }, + appointment_confirmation: { + title: "Appointment Confirmation", + description: + "Sent to a customer as a Confirmation of an upcoming appointment.", + drivingId: "Appointment Id", + key: "appointment_confirmation", + }, parts_order_confirmation: { title: "Parts Order Confirmation", description: "Parts order template including part details", diff --git a/client/templates/appointment_confirmation/appointment_confirmation.query.gql b/client/templates/appointment_confirmation/appointment_confirmation.query.gql new file mode 100644 index 000000000..6e1c4f5a9 --- /dev/null +++ b/client/templates/appointment_confirmation/appointment_confirmation.query.gql @@ -0,0 +1,11 @@ +query EMAIL_APPOINTMENT_CONFIRMATION($id: uuid!) { + appointments_by_pk(id: $id) { + start + title + job { + ownr_fn + ownr_ln + ownr_ea + } + } +} diff --git a/client/templates/appointment_confirmation/appointment_confirmation.template.html b/client/templates/appointment_confirmation/appointment_confirmation.template.html new file mode 100644 index 000000000..5aa9669c7 --- /dev/null +++ b/client/templates/appointment_confirmation/appointment_confirmation.template.html @@ -0,0 +1,9 @@ +
+

Hello {{appointments_by_pk.job.ownr_fn}},

+

+ This is a confirmation that you have an appointment at + {{appointments_by_pk.start}} to bring your car in for repair. Please email + us at {{bodyshop.email}} if you can't make it.  +

+
+ \ No newline at end of file diff --git a/hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/down.yaml b/hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/down.yaml new file mode 100644 index 000000000..4f0ee9f3c --- /dev/null +++ b/hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/down.yaml @@ -0,0 +1,5 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "appt_length"; + type: run_sql diff --git a/hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/up.yaml b/hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/up.yaml new file mode 100644 index 000000000..4becf570d --- /dev/null +++ b/hasura/migrations/1591214568316_alter_table_public_bodyshops_add_column_appt_length/up.yaml @@ -0,0 +1,6 @@ +- args: + cascade: false + read_only: false + sql: ALTER TABLE "public"."bodyshops" ADD COLUMN "appt_length" integer NOT NULL + DEFAULT 60; + type: run_sql diff --git a/hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/down.yaml b/hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/down.yaml new file mode 100644 index 000000000..c7c8f849d --- /dev/null +++ b/hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/down.yaml @@ -0,0 +1,51 @@ +- args: + role: user + table: + name: bodyshops + schema: public + type: drop_select_permission +- args: + permission: + allow_aggregations: false + columns: + - accountingconfig + - address1 + - address2 + - 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 + - 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 diff --git a/hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/up.yaml b/hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/up.yaml new file mode 100644 index 000000000..26ce89aa9 --- /dev/null +++ b/hasura/migrations/1591214578485_update_permission_user_public_table_bodyshops/up.yaml @@ -0,0 +1,52 @@ +- 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 + - 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 diff --git a/hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/down.yaml b/hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/down.yaml new file mode 100644 index 000000000..86c1ba881 --- /dev/null +++ b/hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/down.yaml @@ -0,0 +1,49 @@ +- args: + role: user + table: + name: bodyshops + schema: public + type: drop_update_permission +- args: + permission: + columns: + - accountingconfig + - address1 + - address2 + - 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 + - production_config + - shopname + - shoprates + - state + - state_tax_id + - updated_at + - zip_post + filter: + associations: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + localPresets: + - key: "" + value: "" + set: {} + role: user + table: + name: bodyshops + schema: public + type: create_update_permission diff --git a/hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/up.yaml b/hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/up.yaml new file mode 100644 index 000000000..9c3945d54 --- /dev/null +++ b/hasura/migrations/1591214587491_update_permission_user_public_table_bodyshops/up.yaml @@ -0,0 +1,50 @@ +- args: + role: user + table: + name: bodyshops + schema: public + type: drop_update_permission +- args: + permission: + 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 + - production_config + - shopname + - shoprates + - state + - state_tax_id + - updated_at + - zip_post + filter: + associations: + bodyshop: + associations: + user: + authid: + _eq: X-Hasura-User-Id + localPresets: + - key: "" + value: "" + set: {} + role: user + table: + name: bodyshops + schema: public + type: create_update_permission diff --git a/hasura/migrations/metadata.yaml b/hasura/migrations/metadata.yaml index 8df2ef107..1b83cbd37 100644 --- a/hasura/migrations/metadata.yaml +++ b/hasura/migrations/metadata.yaml @@ -447,6 +447,7 @@ tables: - accountingconfig - address1 - address2 + - appt_length - city - country - created_at @@ -486,6 +487,7 @@ tables: - accountingconfig - address1 - address2 + - appt_length - city - country - created_at diff --git a/package.json b/package.json index ea3c85777..2e5a2af80 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "firebase-admin": "^8.11.0", "graphql-request": "^1.8.2", "handlebars": "^4.7.6", + "lodash": "^4.17.15", + "moment": "^2.26.0", "nodemailer": "^6.4.4", "phone": "^2.4.8", "twilio": "^3.41.1", diff --git a/server/render/renderHandlebars.js b/server/render/renderHandlebars.js index fbe6c52e9..32bc16c77 100644 --- a/server/render/renderHandlebars.js +++ b/server/render/renderHandlebars.js @@ -1,13 +1,73 @@ const path = require("path"); +const moment = require("moment"); require("dotenv").config({ path: path.resolve( process.cwd(), `.env.${process.env.NODE_ENV || "development"}` ), }); - +var _ = require("lodash"); const Handlebars = require("handlebars"); +Handlebars.registerHelper("moment", function (context, block) { + if (context && context.hash) { + block = _.cloneDeep(context); + context = undefined; + } + var date = moment(context); + + if (block.hash.timezone) { + date.tz(block.hash.timezone); + } + + var hasFormat = false; + + // Reset the language back to default before doing anything else + date.locale("en"); + + for (var i in block.hash) { + if (i === "format") { + hasFormat = true; + } else if (date[i]) { + date = date[i](block.hash[i]); + } else { + console.log('moment.js does not support "' + i + '"'); + } + } + + if (hasFormat) { + date = date.format(block.hash.format); + } + return date; +}); + +Handlebars.registerHelper("duration", function (context, block) { + if (context && context.hash) { + block = _.cloneDeep(context); + context = 0; + } + var duration = moment.duration(context); + var hasFormat = false; + + // Reset the language back to default before doing anything else + duration = duration.lang("en"); + + for (var i in block.hash) { + if (i === "format") { + hasFormat = true; + } else if (duration[i]) { + duration = duration[i](block.hash[i]); + } else { + console.log('moment.js duration does not support "' + i + '"'); + } + } + + if (hasFormat) { + duration = duration.format(block.hash.format); + } + return duration; +}); + exports.render = (req, res) => { //Perform request validation let view; diff --git a/yarn.lock b/yarn.lock index d9fb779b4..50c539069 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1992,6 +1992,11 @@ mkdirp@^0.5.1: dependencies: minimist "0.0.8" +moment@^2.26.0: + version "2.26.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" + integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"