Added length of appointment to config + fixed appointments not showing in scheduling modal + added appointment confirmation template. BOD-141 BOD-149 BOD-148
This commit is contained in:
@@ -280,6 +280,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>smartscheduling</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>viewjob</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -353,6 +374,27 @@
|
||||
<folder_node>
|
||||
<name>fields</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>time</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>title</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -421,6 +463,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>history</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>nodateselected</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -875,6 +938,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>appt_length</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>city</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -8,20 +8,21 @@ export default function EmailOverlayComponent({
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
To:
|
||||
<Input
|
||||
defaultValue={messageOptions.to}
|
||||
value={messageOptions.to}
|
||||
onChange={handleConfigChange}
|
||||
name="to"
|
||||
/>
|
||||
CC
|
||||
CC:
|
||||
<Input
|
||||
defaultValue={messageOptions.cc}
|
||||
value={messageOptions.cc}
|
||||
onChange={handleConfigChange}
|
||||
name="cc"
|
||||
/>
|
||||
Subject
|
||||
Subject:
|
||||
<Input
|
||||
defaultValue={messageOptions.subject}
|
||||
value={messageOptions.subject}
|
||||
onChange={handleConfigChange}
|
||||
name="subject"
|
||||
/>
|
||||
|
||||
@@ -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 }}
|
||||
>
|
||||
<LoadingSpinner loading={loading}>
|
||||
<EmailOverlayComponent
|
||||
handleConfigChange={handleConfigChange}
|
||||
@@ -102,10 +104,10 @@ export function EmailOverlayContainer({
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(messageOptions.html);
|
||||
navigator.clipboard.writeText(messageOptions.html);
|
||||
}}>
|
||||
Get HTML
|
||||
}}
|
||||
>
|
||||
Copy HTML
|
||||
</button>
|
||||
</LoadingSpinner>
|
||||
</Modal>
|
||||
|
||||
@@ -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 (
|
||||
<Input
|
||||
{...props}
|
||||
addonAfter={
|
||||
props.email ? (
|
||||
<a href={`mailto:${props.email}`}>
|
||||
props.defaultValue ? (
|
||||
<a href={`mailto:${props.defaultValue}`}>
|
||||
<MailFilled />
|
||||
</a>
|
||||
) : (
|
||||
|
||||
@@ -46,7 +46,7 @@ export function JobsDetailHeader({
|
||||
|
||||
const tombstoneTitle = (
|
||||
<div>
|
||||
<Avatar size='large' alt='Vehicle Image' src={CarImage} />
|
||||
<Avatar size="large" alt="Vehicle Image" src={CarImage} />
|
||||
{job.ro_number
|
||||
? `${t("jobs.fields.ro_number")} ${job.ro_number}`
|
||||
: `EST-${job.est_number}`}
|
||||
@@ -57,7 +57,8 @@ export function JobsDetailHeader({
|
||||
<Menu
|
||||
onClick={(e) => {
|
||||
updateJobStatus(e.key);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_ro_statuses.statuses.map((item) => (
|
||||
<Menu.Item key={item}>{item}</Menu.Item>
|
||||
))}
|
||||
@@ -65,12 +66,12 @@ export function JobsDetailHeader({
|
||||
);
|
||||
|
||||
const menuExtra = [
|
||||
<Dropdown overlay={statusmenu} key='changestatus'>
|
||||
<Dropdown overlay={statusmenu} key="changestatus">
|
||||
<Button>
|
||||
{t("jobs.actions.changestatus")} <DownCircleFilled />
|
||||
</Button>
|
||||
</Dropdown>,
|
||||
<Badge key='schedule' count={job.appointments_aggregate.aggregate.count}>
|
||||
<Badge key="schedule" count={job.appointments_aggregate.aggregate.count}>
|
||||
<Button
|
||||
//TODO Enabled logic based on status.
|
||||
onClick={() => {
|
||||
@@ -78,15 +79,17 @@ export function JobsDetailHeader({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
},
|
||||
});
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.schedule")}
|
||||
</Button>
|
||||
</Badge>,
|
||||
<Button
|
||||
key='convert'
|
||||
type='dashed'
|
||||
key="convert"
|
||||
type="dashed"
|
||||
disabled={job.converted}
|
||||
onClick={() => {
|
||||
mutationConvertJob({
|
||||
@@ -98,11 +101,12 @@ export function JobsDetailHeader({
|
||||
message: t("jobs.successes.converted"),
|
||||
});
|
||||
});
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>,
|
||||
<JobsDetailHeaderActions key='actions' job={job} refetch={refetch} />,
|
||||
<Button type='primary' key='submit' htmlType='submit'>
|
||||
<JobsDetailHeaderActions key="actions" job={job} refetch={refetch} />,
|
||||
<Button type="primary" key="submit" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>,
|
||||
];
|
||||
@@ -115,48 +119,53 @@ export function JobsDetailHeader({
|
||||
title={tombstoneTitle}
|
||||
//subTitle={tombstoneSubtitle}
|
||||
tags={
|
||||
<span key='job-status'>
|
||||
{job.status ? <Tag color='blue'>{job.status}</Tag> : null}
|
||||
<span key="job-status">
|
||||
{job.status ? <Tag color="blue">{job.status}</Tag> : null}
|
||||
{job.inproduction ? (
|
||||
<Tag color='#f50'>{t("jobs.labels.inproduction")}</Tag>
|
||||
<Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>
|
||||
) : null}
|
||||
<OwnerTagPopoverComponent job={job} />
|
||||
<VehicleTagPopoverComponent job={job} />
|
||||
<BarcodePopup value={job.id} />
|
||||
</span>
|
||||
}
|
||||
extra={menuExtra}>
|
||||
<Descriptions size='small' column={5}>
|
||||
<Descriptions.Item key='total' label={t("jobs.fields.repairtotal")}>
|
||||
extra={menuExtra}
|
||||
>
|
||||
<Descriptions size="small" column={5}>
|
||||
<Descriptions.Item key="total" label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item
|
||||
key='custowing'
|
||||
label={t("jobs.fields.customerowing")}>
|
||||
key="custowing"
|
||||
label={t("jobs.fields.customerowing")}
|
||||
>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item
|
||||
key='scp'
|
||||
label={t("jobs.fields.specialcoveragepolicy")}>
|
||||
key="scp"
|
||||
label={t("jobs.fields.specialcoveragepolicy")}
|
||||
>
|
||||
<Checkbox checked={job.special_coverage_policy} />
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item
|
||||
key='sched_comp'
|
||||
label={t("jobs.fields.scheduled_completion")}>
|
||||
key="sched_comp"
|
||||
label={t("jobs.fields.scheduled_completion")}
|
||||
>
|
||||
{job.scheduled_completion ? (
|
||||
<Moment format='MM/DD/YYYY'>{job.scheduled_completion}</Moment>
|
||||
<Moment format="MM/DD/YYYY">{job.scheduled_completion}</Moment>
|
||||
) : null}
|
||||
</Descriptions.Item>
|
||||
|
||||
<Descriptions.Item key='servicecar' label={t("jobs.fields.servicecar")}>
|
||||
<Descriptions.Item key="servicecar" label={t("jobs.fields.servicecar")}>
|
||||
{job.cccontracts &&
|
||||
job.cccontracts.map((item) => (
|
||||
<Link
|
||||
key={item.id}
|
||||
to={`/manage/courtesycars/contracts/${item.id}`}>
|
||||
to={`/manage/courtesycars/contracts/${item.id}`}
|
||||
>
|
||||
<div>{`${item.agreementnumber} - ${item.start} - ${item.scheduledreturn}`}</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -15,16 +15,16 @@ export default function ScheduleCalendarWrapperComponent({
|
||||
refetch,
|
||||
defaultView,
|
||||
setDateRangeCallback,
|
||||
date,
|
||||
...otherProps
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Calendar
|
||||
events={data}
|
||||
defaultView={search.view || defaultView || "week"}
|
||||
date={new Date(search.date || Date.now())}
|
||||
date={new Date(date || search.date || Date.now())}
|
||||
onNavigate={(date, view, action) => {
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<ScheduleCalendarWrapperComponent
|
||||
events={data}
|
||||
defaultView="day"
|
||||
views={["day"]}
|
||||
style={{ height: "40vh" }}
|
||||
defaultDate={new Date(day)}
|
||||
//onNavigate={e => console.log("e", e)}
|
||||
date={day}
|
||||
/>
|
||||
);
|
||||
else return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||
|
||||
@@ -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 <div>{t("appointments.labels.nodateselected")}</div>;
|
||||
if (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
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 (
|
||||
<ScheduleDayViewComponent data={data ? normalizedData : null} day={day} />
|
||||
<ScheduleDayViewComponent data={data ? normalizedData : []} day={day} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Row>
|
||||
<Col span={14}>
|
||||
<Tabs defaultActiveKey="1">
|
||||
<Tabs.TabPane tab="SMART Scheduling" key="auto">
|
||||
Automatic Job Selection.
|
||||
<button onClick={handleAuto}>Get dates.</button>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Manual Scheduling" key="manual">
|
||||
<Row>
|
||||
Manual Job Selection Scheduled Time
|
||||
<div style={{ height: "300px" }}>
|
||||
<DateTimePicker
|
||||
value={appData.start}
|
||||
onChange={(e) => {
|
||||
setAppData({ ...appData, start: e });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
<div style={{ display: "flex", alignContent: "middle" }}>
|
||||
<strong>{t("appointments.fields.time")}</strong>
|
||||
<DateTimePicker
|
||||
value={appData.start}
|
||||
onChange={(e) => {
|
||||
setAppData({ ...appData, start: e });
|
||||
}}
|
||||
/>
|
||||
<Button onClick={handleAuto}>
|
||||
{t("appointments.actions.smartscheduling")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{t("appointments.labels.history")}
|
||||
<ScheduleExistingAppointmentsList
|
||||
existingAppointments={existingAppointments}
|
||||
/>
|
||||
{
|
||||
//TODO Build out notifications.
|
||||
}
|
||||
|
||||
<Checkbox
|
||||
defaultChecked={formData.notifyCustomer}
|
||||
onChange={(e) =>
|
||||
@@ -70,6 +64,11 @@ export default function ScheduleJobModalComponent({
|
||||
>
|
||||
{t("jobs.labels.appointmentconfirmation")}
|
||||
</Checkbox>
|
||||
<EmailInput
|
||||
defaultValue={formData.email}
|
||||
title={t("owner.fields.ownr_ea")}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<ScheduleDayViewContainer day={appData.start} />
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -9,71 +9,92 @@ export default function ShopInfoComponent({ form }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Collapse defaultActiveKey='shopinfo'>
|
||||
<Collapse.Panel key='shopinfo' header={t("bodyshop.labels.shopinfo")}>
|
||||
<Form.Item label={t("bodyshop.fields.shopname")} name='shopname'>
|
||||
<Collapse defaultActiveKey="shopinfo">
|
||||
<Collapse.Panel key="shopinfo" header={t("bodyshop.labels.shopinfo")}>
|
||||
<Form.Item label={t("bodyshop.fields.shopname")} name="shopname">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.address1")} name='address1'>
|
||||
<Form.Item label={t("bodyshop.fields.address1")} name="address1">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("bodyshop.fields.address2")} name='address2'>
|
||||
<Form.Item label={t("bodyshop.fields.address2")} name="address2">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.city")} name='city'>
|
||||
<Form.Item label={t("bodyshop.fields.city")} name="city">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.state")} name='state'>
|
||||
<Form.Item label={t("bodyshop.fields.state")} name="state">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.zip_post")} name='zip_post'>
|
||||
<Form.Item label={t("bodyshop.fields.zip_post")} name="zip_post">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.country")} name='country'>
|
||||
<Form.Item label={t("bodyshop.fields.country")} name="country">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.email")} name='email'>
|
||||
<Form.Item label={t("bodyshop.fields.email")} name="email">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.federal_tax_id")}
|
||||
name='federal_tax_id'>
|
||||
name="federal_tax_id"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.insurance_vendor_id")}
|
||||
name='insurance_vendor_id'>
|
||||
name="insurance_vendor_id"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.logo_img_path")}
|
||||
name='logo_img_path'>
|
||||
name="logo_img_path"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.state_tax_id")}
|
||||
name='state_tax_id'>
|
||||
name="state_tax_id"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["invoice_tax_rates", "federal_tax_rate"]}>
|
||||
name={["invoice_tax_rates", "federal_tax_rate"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
name={["invoice_tax_rates", "state_tax_rate"]}>
|
||||
name={["invoice_tax_rates", "state_tax_rate"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||
name={["invoice_tax_rates", "local_tax_rate"]}>
|
||||
name={["invoice_tax_rates", "local_tax_rate"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.appt_length")}
|
||||
name={"appt_length"}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber min={15} precisio={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
@@ -82,7 +103,8 @@ export default function ShopInfoComponent({ form }) {
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["accountingconfig", "tiers"]}>
|
||||
name={["accountingconfig", "tiers"]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={2}>2</Radio>
|
||||
<Radio value={3}>3</Radio>
|
||||
@@ -101,13 +123,15 @@ export default function ShopInfoComponent({ form }) {
|
||||
message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}>
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<Radio.Group
|
||||
disabled={
|
||||
form.getFieldValue(["accountingconfig", "tiers"]) === 3
|
||||
}>
|
||||
<Radio value='name'>{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value='source'>
|
||||
}
|
||||
>
|
||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value="source">
|
||||
{t("bodyshop.labels.2tiersource")}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
@@ -117,18 +141,21 @@ export default function ShopInfoComponent({ form }) {
|
||||
</Form.Item>
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel
|
||||
key='roStatus'
|
||||
header={t("bodyshop.labels.jobstatuses")}>
|
||||
key="roStatus"
|
||||
header={t("bodyshop.labels.jobstatuses")}
|
||||
>
|
||||
<ShopInfoROStatusComponent form={form} />
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel
|
||||
key='orderStatus'
|
||||
header={t("bodyshop.labels.orderstatuses")}>
|
||||
key="orderStatus"
|
||||
header={t("bodyshop.labels.orderstatuses")}
|
||||
>
|
||||
<ShopInfoOrderStatusComponent form={form} />
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel
|
||||
key='responsibilityCenters'
|
||||
header={t("bodyshop.labels.responsibilitycenters.title")}>
|
||||
key="responsibilityCenters"
|
||||
header={t("bodyshop.labels.responsibilitycenters.title")}
|
||||
>
|
||||
<ShopInfoResponsibilityCenterComponent form={form} />
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
|
||||
@@ -78,6 +78,7 @@ export const UPDATE_SHOP = gql`
|
||||
textid
|
||||
production_config
|
||||
invoice_tax_rates
|
||||
appt_length
|
||||
employees {
|
||||
id
|
||||
first_name
|
||||
|
||||
@@ -28,9 +28,10 @@ export default function CsiContainerPage() {
|
||||
return (
|
||||
<div>
|
||||
<Result
|
||||
status='error'
|
||||
status="error"
|
||||
title={t("csi.errors.notfoundtitle")}
|
||||
subTitle={t("csi.errors.notfoundsubtitle")}>
|
||||
subTitle={t("csi.errors.notfoundsubtitle")}
|
||||
>
|
||||
{error ? (
|
||||
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
|
||||
) : null}
|
||||
@@ -70,18 +71,20 @@ export default function CsiContainerPage() {
|
||||
|
||||
return (
|
||||
<Layout
|
||||
style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
|
||||
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
|
||||
{bodyshop.logo_img_path ? (
|
||||
<img src={bodyshop.logo_img_path} alt='Logo' />
|
||||
<img src={bodyshop.logo_img_path} alt="Logo" />
|
||||
) : null}
|
||||
<div>
|
||||
<div style={{ margin: "2em" }}>
|
||||
<strong>{bodyshop.shopname || ""}</strong>
|
||||
<div>{`${bodyshop.address1 || ""}`}</div>
|
||||
<div>{`${bodyshop.address2 || ""}`}</div>
|
||||
@@ -93,13 +96,15 @@ export default function CsiContainerPage() {
|
||||
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
|
||||
<strong>{`Hi ${job.ownr_fn || ""}!`}</strong>
|
||||
<Typography.Paragraph>
|
||||
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.`}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
{submitting.error ? (
|
||||
<AlertComponent message={submitting.error} type='error' />
|
||||
<AlertComponent message={submitting.error} type="error" />
|
||||
) : null}
|
||||
|
||||
{submitting.submitted ? (
|
||||
@@ -109,9 +114,10 @@ export default function CsiContainerPage() {
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status='success'
|
||||
status="success"
|
||||
title={t("csi.successes.submitted")}
|
||||
subTitle={t("csi.successes.submittedsub")}
|
||||
/>
|
||||
@@ -123,13 +129,15 @@ export default function CsiContainerPage() {
|
||||
margin: "2em 4em",
|
||||
padding: "2em",
|
||||
overflowY: "auto",
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Form form={form} onFinish={handleFinish}>
|
||||
<ConfigFormComponents componentList={csiquestions} />
|
||||
<Button
|
||||
loading={submitting.loading}
|
||||
type='primary'
|
||||
htmlType='submit'>
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
{t("general.actions.submit")}
|
||||
</Button>
|
||||
</Form>
|
||||
@@ -137,7 +145,7 @@ export default function CsiContainerPage() {
|
||||
)}
|
||||
|
||||
<Layout.Footer>
|
||||
Copyright ImEX.Online. Survey ID: {surveyId}
|
||||
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
query EMAIL_APPOINTMENT_CONFIRMATION($id: uuid!) {
|
||||
appointments_by_pk(id: $id) {
|
||||
start
|
||||
title
|
||||
job {
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_ea
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div style="font-family: Arial, Helvetica, sans-serif;">
|
||||
<p style="text-align: center;">Hello {{appointments_by_pk.job.ownr_fn}},</p>
|
||||
<p style="text-align: center;">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- args:
|
||||
cascade: false
|
||||
read_only: false
|
||||
sql: ALTER TABLE "public"."bodyshops" DROP COLUMN "appt_length";
|
||||
type: run_sql
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user