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>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</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>
|
<concept_node>
|
||||||
<name>viewjob</name>
|
<name>viewjob</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -353,6 +374,27 @@
|
|||||||
<folder_node>
|
<folder_node>
|
||||||
<name>fields</name>
|
<name>fields</name>
|
||||||
<children>
|
<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>
|
<concept_node>
|
||||||
<name>title</name>
|
<name>title</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -421,6 +463,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</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>
|
<concept_node>
|
||||||
<name>nodateselected</name>
|
<name>nodateselected</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
@@ -875,6 +938,27 @@
|
|||||||
</translation>
|
</translation>
|
||||||
</translations>
|
</translations>
|
||||||
</concept_node>
|
</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>
|
<concept_node>
|
||||||
<name>city</name>
|
<name>city</name>
|
||||||
<definition_loaded>false</definition_loaded>
|
<definition_loaded>false</definition_loaded>
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ export default function EmailOverlayComponent({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
To:
|
||||||
<Input
|
<Input
|
||||||
defaultValue={messageOptions.to}
|
value={messageOptions.to}
|
||||||
onChange={handleConfigChange}
|
onChange={handleConfigChange}
|
||||||
name="to"
|
name="to"
|
||||||
/>
|
/>
|
||||||
CC
|
CC:
|
||||||
<Input
|
<Input
|
||||||
defaultValue={messageOptions.cc}
|
value={messageOptions.cc}
|
||||||
onChange={handleConfigChange}
|
onChange={handleConfigChange}
|
||||||
name="cc"
|
name="cc"
|
||||||
/>
|
/>
|
||||||
Subject
|
Subject:
|
||||||
<Input
|
<Input
|
||||||
defaultValue={messageOptions.subject}
|
value={messageOptions.subject}
|
||||||
onChange={handleConfigChange}
|
onChange={handleConfigChange}
|
||||||
name="subject"
|
name="subject"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export function EmailOverlayContainer({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
const defaultEmailFrom = {
|
const defaultEmailFrom = {
|
||||||
from: {
|
from: {
|
||||||
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
name: bodyshop.shopname || EmailSettings.fromNameDefault,
|
||||||
@@ -45,21 +46,19 @@ export function EmailOverlayContainer({
|
|||||||
html: "",
|
html: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = async () => {
|
||||||
//sendEmail(messageOptions);
|
setSending(true);
|
||||||
axios
|
try {
|
||||||
.post("/sendemail", messageOptions)
|
const emailResponse = await axios.post("/sendemail", messageOptions);
|
||||||
.then((response) => {
|
notification["success"]({ message: t("emails.successes.sent") });
|
||||||
console.log(JSON.stringify(response));
|
toggleEmailOverlayVisible();
|
||||||
notification["success"]({ message: t("emails.successes.sent") });
|
} catch (error) {
|
||||||
toggleEmailOverlayVisible();
|
console.log(JSON.stringify(error));
|
||||||
})
|
notification["error"]({
|
||||||
.catch((error) => {
|
message: t("emails.errors.notsent", { message: error.message }),
|
||||||
console.log(JSON.stringify(error));
|
|
||||||
notification["error"]({
|
|
||||||
message: t("emails.errors.notsent", { message: error.message }),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
setSending(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigChange = (event) => {
|
const handleConfigChange = (event) => {
|
||||||
@@ -72,6 +71,7 @@ export function EmailOverlayContainer({
|
|||||||
|
|
||||||
const render = async () => {
|
const render = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
console.log("emailConfig", emailConfig);
|
||||||
let html = await RenderTemplate(emailConfig.template, bodyshop);
|
let html = await RenderTemplate(emailConfig.template, bodyshop);
|
||||||
setMessageOptions({
|
setMessageOptions({
|
||||||
...emailConfig.messageOptions,
|
...emailConfig.messageOptions,
|
||||||
@@ -93,7 +93,9 @@ export function EmailOverlayContainer({
|
|||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleEmailOverlayVisible();
|
toggleEmailOverlayVisible();
|
||||||
}}>
|
}}
|
||||||
|
okButtonProps={{ loading: sending }}
|
||||||
|
>
|
||||||
<LoadingSpinner loading={loading}>
|
<LoadingSpinner loading={loading}>
|
||||||
<EmailOverlayComponent
|
<EmailOverlayComponent
|
||||||
handleConfigChange={handleConfigChange}
|
handleConfigChange={handleConfigChange}
|
||||||
@@ -102,10 +104,10 @@ export function EmailOverlayContainer({
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(messageOptions.html);
|
|
||||||
navigator.clipboard.writeText(messageOptions.html);
|
navigator.clipboard.writeText(messageOptions.html);
|
||||||
}}>
|
}}
|
||||||
Get HTML
|
>
|
||||||
|
Copy HTML
|
||||||
</button>
|
</button>
|
||||||
</LoadingSpinner>
|
</LoadingSpinner>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Input } from "antd";
|
import { Input } from "antd";
|
||||||
import { MailFilled } from "@ant-design/icons";
|
import { MailFilled } from "@ant-design/icons";
|
||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
function FormItemEmail(props, ref) {
|
function FormItemEmail(props, ref) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
addonAfter={
|
addonAfter={
|
||||||
props.email ? (
|
props.defaultValue ? (
|
||||||
<a href={`mailto:${props.email}`}>
|
<a href={`mailto:${props.defaultValue}`}>
|
||||||
<MailFilled />
|
<MailFilled />
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function JobsDetailHeader({
|
|||||||
|
|
||||||
const tombstoneTitle = (
|
const tombstoneTitle = (
|
||||||
<div>
|
<div>
|
||||||
<Avatar size='large' alt='Vehicle Image' src={CarImage} />
|
<Avatar size="large" alt="Vehicle Image" src={CarImage} />
|
||||||
{job.ro_number
|
{job.ro_number
|
||||||
? `${t("jobs.fields.ro_number")} ${job.ro_number}`
|
? `${t("jobs.fields.ro_number")} ${job.ro_number}`
|
||||||
: `EST-${job.est_number}`}
|
: `EST-${job.est_number}`}
|
||||||
@@ -57,7 +57,8 @@ export function JobsDetailHeader({
|
|||||||
<Menu
|
<Menu
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
updateJobStatus(e.key);
|
updateJobStatus(e.key);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{bodyshop.md_ro_statuses.statuses.map((item) => (
|
{bodyshop.md_ro_statuses.statuses.map((item) => (
|
||||||
<Menu.Item key={item}>{item}</Menu.Item>
|
<Menu.Item key={item}>{item}</Menu.Item>
|
||||||
))}
|
))}
|
||||||
@@ -65,12 +66,12 @@ export function JobsDetailHeader({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const menuExtra = [
|
const menuExtra = [
|
||||||
<Dropdown overlay={statusmenu} key='changestatus'>
|
<Dropdown overlay={statusmenu} key="changestatus">
|
||||||
<Button>
|
<Button>
|
||||||
{t("jobs.actions.changestatus")} <DownCircleFilled />
|
{t("jobs.actions.changestatus")} <DownCircleFilled />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>,
|
</Dropdown>,
|
||||||
<Badge key='schedule' count={job.appointments_aggregate.aggregate.count}>
|
<Badge key="schedule" count={job.appointments_aggregate.aggregate.count}>
|
||||||
<Button
|
<Button
|
||||||
//TODO Enabled logic based on status.
|
//TODO Enabled logic based on status.
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -78,15 +79,17 @@ export function JobsDetailHeader({
|
|||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
context: {
|
context: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
|
job: job,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{t("jobs.actions.schedule")}
|
{t("jobs.actions.schedule")}
|
||||||
</Button>
|
</Button>
|
||||||
</Badge>,
|
</Badge>,
|
||||||
<Button
|
<Button
|
||||||
key='convert'
|
key="convert"
|
||||||
type='dashed'
|
type="dashed"
|
||||||
disabled={job.converted}
|
disabled={job.converted}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutationConvertJob({
|
mutationConvertJob({
|
||||||
@@ -98,11 +101,12 @@ export function JobsDetailHeader({
|
|||||||
message: t("jobs.successes.converted"),
|
message: t("jobs.successes.converted"),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{t("jobs.actions.convert")}
|
{t("jobs.actions.convert")}
|
||||||
</Button>,
|
</Button>,
|
||||||
<JobsDetailHeaderActions key='actions' job={job} refetch={refetch} />,
|
<JobsDetailHeaderActions key="actions" job={job} refetch={refetch} />,
|
||||||
<Button type='primary' key='submit' htmlType='submit'>
|
<Button type="primary" key="submit" htmlType="submit">
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>,
|
</Button>,
|
||||||
];
|
];
|
||||||
@@ -115,48 +119,53 @@ export function JobsDetailHeader({
|
|||||||
title={tombstoneTitle}
|
title={tombstoneTitle}
|
||||||
//subTitle={tombstoneSubtitle}
|
//subTitle={tombstoneSubtitle}
|
||||||
tags={
|
tags={
|
||||||
<span key='job-status'>
|
<span key="job-status">
|
||||||
{job.status ? <Tag color='blue'>{job.status}</Tag> : null}
|
{job.status ? <Tag color="blue">{job.status}</Tag> : null}
|
||||||
{job.inproduction ? (
|
{job.inproduction ? (
|
||||||
<Tag color='#f50'>{t("jobs.labels.inproduction")}</Tag>
|
<Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
<OwnerTagPopoverComponent job={job} />
|
<OwnerTagPopoverComponent job={job} />
|
||||||
<VehicleTagPopoverComponent job={job} />
|
<VehicleTagPopoverComponent job={job} />
|
||||||
<BarcodePopup value={job.id} />
|
<BarcodePopup value={job.id} />
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
extra={menuExtra}>
|
extra={menuExtra}
|
||||||
<Descriptions size='small' column={5}>
|
>
|
||||||
<Descriptions.Item key='total' label={t("jobs.fields.repairtotal")}>
|
<Descriptions size="small" column={5}>
|
||||||
|
<Descriptions.Item key="total" label={t("jobs.fields.repairtotal")}>
|
||||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
key='custowing'
|
key="custowing"
|
||||||
label={t("jobs.fields.customerowing")}>
|
label={t("jobs.fields.customerowing")}
|
||||||
|
>
|
||||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
key='scp'
|
key="scp"
|
||||||
label={t("jobs.fields.specialcoveragepolicy")}>
|
label={t("jobs.fields.specialcoveragepolicy")}
|
||||||
|
>
|
||||||
<Checkbox checked={job.special_coverage_policy} />
|
<Checkbox checked={job.special_coverage_policy} />
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
key='sched_comp'
|
key="sched_comp"
|
||||||
label={t("jobs.fields.scheduled_completion")}>
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
>
|
||||||
{job.scheduled_completion ? (
|
{job.scheduled_completion ? (
|
||||||
<Moment format='MM/DD/YYYY'>{job.scheduled_completion}</Moment>
|
<Moment format="MM/DD/YYYY">{job.scheduled_completion}</Moment>
|
||||||
) : null}
|
) : null}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
||||||
<Descriptions.Item key='servicecar' label={t("jobs.fields.servicecar")}>
|
<Descriptions.Item key="servicecar" label={t("jobs.fields.servicecar")}>
|
||||||
{job.cccontracts &&
|
{job.cccontracts &&
|
||||||
job.cccontracts.map((item) => (
|
job.cccontracts.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.id}
|
key={item.id}
|
||||||
to={`/manage/courtesycars/contracts/${item.id}`}>
|
to={`/manage/courtesycars/contracts/${item.id}`}
|
||||||
|
>
|
||||||
<div>{`${item.agreementnumber} - ${item.start} - ${item.scheduledreturn}`}</div>
|
<div>{`${item.agreementnumber} - ${item.start} - ${item.scheduledreturn}`}</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ export default function ScheduleCalendarWrapperComponent({
|
|||||||
refetch,
|
refetch,
|
||||||
defaultView,
|
defaultView,
|
||||||
setDateRangeCallback,
|
setDateRangeCallback,
|
||||||
|
date,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) {
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Calendar
|
<Calendar
|
||||||
events={data}
|
events={data}
|
||||||
defaultView={search.view || defaultView || "week"}
|
defaultView={search.view || defaultView || "week"}
|
||||||
date={new Date(search.date || Date.now())}
|
date={new Date(date || search.date || Date.now())}
|
||||||
onNavigate={(date, view, action) => {
|
onNavigate={(date, view, action) => {
|
||||||
search.date = date.toISOString().substr(0, 10);
|
search.date = date.toISOString().substr(0, 10);
|
||||||
history.push({ search: queryString.stringify(search) });
|
history.push({ search: queryString.stringify(search) });
|
||||||
@@ -45,6 +45,7 @@ export default function ScheduleCalendarWrapperComponent({
|
|||||||
components={{
|
components={{
|
||||||
event: (e) => Event({ event: e.event, refetch: refetch }),
|
event: (e) => Event({ event: e.event, refetch: refetch }),
|
||||||
header: HeaderComponent,
|
header: HeaderComponent,
|
||||||
|
toolbar: null,
|
||||||
}}
|
}}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ import ScheduleCalendarWrapperComponent from "../schedule-calendar-wrapper/sched
|
|||||||
export default function ScheduleDayViewComponent({ data, day }) {
|
export default function ScheduleDayViewComponent({ data, day }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (data)
|
if (data)
|
||||||
//TODO Remove addtional calendar elements from day view.
|
|
||||||
return (
|
return (
|
||||||
<ScheduleCalendarWrapperComponent
|
<ScheduleCalendarWrapperComponent
|
||||||
events={data}
|
events={data}
|
||||||
defaultView="day"
|
defaultView="day"
|
||||||
views={["day"]}
|
views={["day"]}
|
||||||
style={{ height: "40vh" }}
|
style={{ height: "40vh" }}
|
||||||
defaultDate={new Date(day)}
|
date={day}
|
||||||
//onNavigate={e => console.log("e", e)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
else return <div>{t("appointments.labels.nodateselected")}</div>;
|
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 { QUERY_APPOINTMENT_BY_DATE } from "../../graphql/appointments.queries";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
export default function ScheduleDayViewContainer({ day }) {
|
export default function ScheduleDayViewContainer({ day }) {
|
||||||
const { loading, error, data } = useQuery(QUERY_APPOINTMENT_BY_DATE, {
|
const { loading, error, data } = useQuery(QUERY_APPOINTMENT_BY_DATE, {
|
||||||
variables: {
|
variables: {
|
||||||
start: moment(day).startOf("day"),
|
start: moment(day).startOf("day"),
|
||||||
end: moment(day).endOf("day")
|
end: moment(day).endOf("day"),
|
||||||
},
|
},
|
||||||
skip: !day,
|
skip: !!!day,
|
||||||
fetchPolicy: "network-only"
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (!!!day) return <div>{t("appointments.labels.nodateselected")}</div>;
|
||||||
if (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
|
if (loading) return <LoadingSkeleton paragraph={{ rows: 4 }} />;
|
||||||
if (error) return <div>{error.message}</div>;
|
if (error) return <div>{error.message}</div>;
|
||||||
let normalizedData;
|
let normalizedData;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
normalizedData = data.appointments.map(e => {
|
normalizedData = data.appointments.map((e) => {
|
||||||
//Required becuase Hasura returns a string instead of a date object.
|
//Required becuase Hasura returns a string instead of a date object.
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
{},
|
{},
|
||||||
@@ -31,6 +33,6 @@ export default function ScheduleDayViewContainer({ day }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
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 ScheduleExistingAppointmentsList from "../schedule-existing-appointments-list/schedule-existing-appointments-list.component";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { auth } from "../../firebase/firebase.utils";
|
import { auth } from "../../firebase/firebase.utils";
|
||||||
|
import EmailInput from "../form-items-formatted/email-form-item.component";
|
||||||
|
|
||||||
export default function ScheduleJobModalComponent({
|
export default function ScheduleJobModalComponent({
|
||||||
existingAppointments,
|
existingAppointments,
|
||||||
@@ -37,31 +38,24 @@ export default function ScheduleJobModalComponent({
|
|||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={14}>
|
<Col span={14}>
|
||||||
<Tabs defaultActiveKey="1">
|
<div style={{ display: "flex", alignContent: "middle" }}>
|
||||||
<Tabs.TabPane tab="SMART Scheduling" key="auto">
|
<strong>{t("appointments.fields.time")}</strong>
|
||||||
Automatic Job Selection.
|
<DateTimePicker
|
||||||
<button onClick={handleAuto}>Get dates.</button>
|
value={appData.start}
|
||||||
</Tabs.TabPane>
|
onChange={(e) => {
|
||||||
<Tabs.TabPane tab="Manual Scheduling" key="manual">
|
setAppData({ ...appData, start: e });
|
||||||
<Row>
|
}}
|
||||||
Manual Job Selection Scheduled Time
|
/>
|
||||||
<div style={{ height: "300px" }}>
|
<Button onClick={handleAuto}>
|
||||||
<DateTimePicker
|
{t("appointments.actions.smartscheduling")}
|
||||||
value={appData.start}
|
</Button>
|
||||||
onChange={(e) => {
|
</div>
|
||||||
setAppData({ ...appData, start: e });
|
|
||||||
}}
|
{t("appointments.labels.history")}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
<ScheduleExistingAppointmentsList
|
<ScheduleExistingAppointmentsList
|
||||||
existingAppointments={existingAppointments}
|
existingAppointments={existingAppointments}
|
||||||
/>
|
/>
|
||||||
{
|
|
||||||
//TODO Build out notifications.
|
|
||||||
}
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
defaultChecked={formData.notifyCustomer}
|
defaultChecked={formData.notifyCustomer}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -70,6 +64,11 @@ export default function ScheduleJobModalComponent({
|
|||||||
>
|
>
|
||||||
{t("jobs.labels.appointmentconfirmation")}
|
{t("jobs.labels.appointmentconfirmation")}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
<EmailInput
|
||||||
|
defaultValue={formData.email}
|
||||||
|
title={t("owner.fields.ownr_ea")}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={10}>
|
<Col span={10}>
|
||||||
<ScheduleDayViewContainer day={appData.start} />
|
<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 ScheduleJobModalComponent from "./schedule-job-modal.component";
|
||||||
import { useMutation, useQuery } from "@apollo/react-hooks";
|
import { useMutation, useQuery } from "@apollo/react-hooks";
|
||||||
import {
|
import {
|
||||||
@@ -9,12 +9,13 @@ import moment from "moment";
|
|||||||
import { notification, Modal } from "antd";
|
import { notification, Modal } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
|
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
|
||||||
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { selectSchedule } from "../../redux/modals/modals.selectors";
|
import { selectSchedule } from "../../redux/modals/modals.selectors";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -22,22 +23,37 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
|
||||||
|
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||||
});
|
});
|
||||||
export function ScheduleJobModalContainer({
|
export function ScheduleJobModalContainer({
|
||||||
scheduleModal,
|
scheduleModal,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
|
setEmailOptions,
|
||||||
}) {
|
}) {
|
||||||
const { visible, context, actions } = scheduleModal;
|
const { visible, context, actions } = scheduleModal;
|
||||||
const { jobId } = context;
|
const { jobId, job } = context;
|
||||||
const { refetch } = actions;
|
const { refetch } = actions;
|
||||||
|
|
||||||
const [appData, setAppData] = useState({
|
const [appData, setAppData] = useState({
|
||||||
start: null,
|
start: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
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 });
|
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 { t } = useTranslation();
|
||||||
|
|
||||||
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
|
const existingAppointments = useQuery(QUERY_APPOINTMENTS_BY_JOBID, {
|
||||||
@@ -47,47 +63,67 @@ export function ScheduleJobModalContainer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
//TODO Customize the amount of minutes it will add.
|
//TODO Customize the amount of minutes it will add.
|
||||||
const handleOk = () => {
|
const handleOk = async () => {
|
||||||
insertAppointment({
|
const appt = await insertAppointment({
|
||||||
variables: {
|
variables: {
|
||||||
app: {
|
app: {
|
||||||
...appData,
|
...appData,
|
||||||
jobid: jobId,
|
jobid: jobId,
|
||||||
bodyshopid: bodyshop.id,
|
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) {
|
if (!!appt.errors) {
|
||||||
//TODO Implement customer reminder on scheduling.
|
notification["error"]({
|
||||||
alert("Chosed to notify the customer somehow!");
|
message: t("appointments.errors.saving", {
|
||||||
}
|
message: JSON.stringify(appt.errors),
|
||||||
toggleModalVisible();
|
}),
|
||||||
if (refetch) refetch();
|
});
|
||||||
});
|
return;
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
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"]({
|
notification["error"]({
|
||||||
message: t("appointments.errors.saving", {
|
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 (
|
return (
|
||||||
|
|||||||
@@ -9,71 +9,92 @@ export default function ShopInfoComponent({ form }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button type='primary' htmlType='submit'>
|
<Button type="primary" htmlType="submit">
|
||||||
{t("general.actions.save")}
|
{t("general.actions.save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Collapse defaultActiveKey='shopinfo'>
|
<Collapse defaultActiveKey="shopinfo">
|
||||||
<Collapse.Panel key='shopinfo' header={t("bodyshop.labels.shopinfo")}>
|
<Collapse.Panel key="shopinfo" header={t("bodyshop.labels.shopinfo")}>
|
||||||
<Form.Item label={t("bodyshop.fields.shopname")} name='shopname'>
|
<Form.Item label={t("bodyshop.fields.shopname")} name="shopname">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("bodyshop.fields.address1")} name='address1'>
|
<Form.Item label={t("bodyshop.fields.address1")} name="address1">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("bodyshop.fields.address2")} name='address2'>
|
<Form.Item label={t("bodyshop.fields.address2")} name="address2">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("bodyshop.fields.city")} name='city'>
|
<Form.Item label={t("bodyshop.fields.city")} name="city">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("bodyshop.fields.state")} name='state'>
|
<Form.Item label={t("bodyshop.fields.state")} name="state">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</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 />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("bodyshop.fields.country")} name='country'>
|
<Form.Item label={t("bodyshop.fields.country")} name="country">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("bodyshop.fields.email")} name='email'>
|
<Form.Item label={t("bodyshop.fields.email")} name="email">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.federal_tax_id")}
|
label={t("bodyshop.fields.federal_tax_id")}
|
||||||
name='federal_tax_id'>
|
name="federal_tax_id"
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.insurance_vendor_id")}
|
label={t("bodyshop.fields.insurance_vendor_id")}
|
||||||
name='insurance_vendor_id'>
|
name="insurance_vendor_id"
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.logo_img_path")}
|
label={t("bodyshop.fields.logo_img_path")}
|
||||||
name='logo_img_path'>
|
name="logo_img_path"
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.state_tax_id")}
|
label={t("bodyshop.fields.state_tax_id")}
|
||||||
name='state_tax_id'>
|
name="state_tax_id"
|
||||||
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||||
name={["invoice_tax_rates", "federal_tax_rate"]}>
|
name={["invoice_tax_rates", "federal_tax_rate"]}
|
||||||
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||||
name={["invoice_tax_rates", "state_tax_rate"]}>
|
name={["invoice_tax_rates", "state_tax_rate"]}
|
||||||
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||||
name={["invoice_tax_rates", "local_tax_rate"]}>
|
name={["invoice_tax_rates", "local_tax_rate"]}
|
||||||
|
>
|
||||||
<InputNumber />
|
<InputNumber />
|
||||||
</Form.Item>
|
</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
|
<Form.Item
|
||||||
label={t("bodyshop.labels.accountingtiers")}
|
label={t("bodyshop.labels.accountingtiers")}
|
||||||
rules={[
|
rules={[
|
||||||
@@ -82,7 +103,8 @@ export default function ShopInfoComponent({ form }) {
|
|||||||
message: t("general.validation.required"),
|
message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
name={["accountingconfig", "tiers"]}>
|
name={["accountingconfig", "tiers"]}
|
||||||
|
>
|
||||||
<Radio.Group>
|
<Radio.Group>
|
||||||
<Radio value={2}>2</Radio>
|
<Radio value={2}>2</Radio>
|
||||||
<Radio value={3}>3</Radio>
|
<Radio value={3}>3</Radio>
|
||||||
@@ -101,13 +123,15 @@ export default function ShopInfoComponent({ form }) {
|
|||||||
message: t("general.validation.required"),
|
message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
name={["accountingconfig", "twotierpref"]}>
|
name={["accountingconfig", "twotierpref"]}
|
||||||
|
>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
disabled={
|
disabled={
|
||||||
form.getFieldValue(["accountingconfig", "tiers"]) === 3
|
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")}
|
{t("bodyshop.labels.2tiersource")}
|
||||||
</Radio>
|
</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
@@ -117,18 +141,21 @@ export default function ShopInfoComponent({ form }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key='roStatus'
|
key="roStatus"
|
||||||
header={t("bodyshop.labels.jobstatuses")}>
|
header={t("bodyshop.labels.jobstatuses")}
|
||||||
|
>
|
||||||
<ShopInfoROStatusComponent form={form} />
|
<ShopInfoROStatusComponent form={form} />
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key='orderStatus'
|
key="orderStatus"
|
||||||
header={t("bodyshop.labels.orderstatuses")}>
|
header={t("bodyshop.labels.orderstatuses")}
|
||||||
|
>
|
||||||
<ShopInfoOrderStatusComponent form={form} />
|
<ShopInfoOrderStatusComponent form={form} />
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key='responsibilityCenters'
|
key="responsibilityCenters"
|
||||||
header={t("bodyshop.labels.responsibilitycenters.title")}>
|
header={t("bodyshop.labels.responsibilitycenters.title")}
|
||||||
|
>
|
||||||
<ShopInfoResponsibilityCenterComponent form={form} />
|
<ShopInfoResponsibilityCenterComponent form={form} />
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const UPDATE_SHOP = gql`
|
|||||||
textid
|
textid
|
||||||
production_config
|
production_config
|
||||||
invoice_tax_rates
|
invoice_tax_rates
|
||||||
|
appt_length
|
||||||
employees {
|
employees {
|
||||||
id
|
id
|
||||||
first_name
|
first_name
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ export default function CsiContainerPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Result
|
<Result
|
||||||
status='error'
|
status="error"
|
||||||
title={t("csi.errors.notfoundtitle")}
|
title={t("csi.errors.notfoundtitle")}
|
||||||
subTitle={t("csi.errors.notfoundsubtitle")}>
|
subTitle={t("csi.errors.notfoundsubtitle")}
|
||||||
|
>
|
||||||
{error ? (
|
{error ? (
|
||||||
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
|
<div>ERROR: {error.graphQLErrors.map((e) => e.message)}</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -70,18 +71,20 @@ export default function CsiContainerPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
|
style={{ height: "100vh", display: "flex", flexDirection: "column" }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
|
<div style={{ display: "flex", alignItems: "center", margin: "2em" }}>
|
||||||
{bodyshop.logo_img_path ? (
|
{bodyshop.logo_img_path ? (
|
||||||
<img src={bodyshop.logo_img_path} alt='Logo' />
|
<img src={bodyshop.logo_img_path} alt="Logo" />
|
||||||
) : null}
|
) : null}
|
||||||
<div>
|
<div style={{ margin: "2em" }}>
|
||||||
<strong>{bodyshop.shopname || ""}</strong>
|
<strong>{bodyshop.shopname || ""}</strong>
|
||||||
<div>{`${bodyshop.address1 || ""}`}</div>
|
<div>{`${bodyshop.address1 || ""}`}</div>
|
||||||
<div>{`${bodyshop.address2 || ""}`}</div>
|
<div>{`${bodyshop.address2 || ""}`}</div>
|
||||||
@@ -93,13 +96,15 @@ export default function CsiContainerPage() {
|
|||||||
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
|
<Typography.Title>{t("csi.labels.title")}</Typography.Title>
|
||||||
<strong>{`Hi ${job.ownr_fn || ""}!`}</strong>
|
<strong>{`Hi ${job.ownr_fn || ""}!`}</strong>
|
||||||
<Typography.Paragraph>
|
<Typography.Paragraph>
|
||||||
At {bodyshop.shopname || ""}, we value your feedback. We would love to
|
{`At ${
|
||||||
hear what you have to say. Please fill out the form below.
|
bodyshop.shopname || ""
|
||||||
|
}, we value your feedback. We would love to
|
||||||
|
hear what you have to say. Please fill out the form below.`}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{submitting.error ? (
|
{submitting.error ? (
|
||||||
<AlertComponent message={submitting.error} type='error' />
|
<AlertComponent message={submitting.error} type="error" />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{submitting.submitted ? (
|
{submitting.submitted ? (
|
||||||
@@ -109,9 +114,10 @@ export default function CsiContainerPage() {
|
|||||||
margin: "2em 4em",
|
margin: "2em 4em",
|
||||||
padding: "2em",
|
padding: "2em",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Result
|
<Result
|
||||||
status='success'
|
status="success"
|
||||||
title={t("csi.successes.submitted")}
|
title={t("csi.successes.submitted")}
|
||||||
subTitle={t("csi.successes.submittedsub")}
|
subTitle={t("csi.successes.submittedsub")}
|
||||||
/>
|
/>
|
||||||
@@ -123,13 +129,15 @@ export default function CsiContainerPage() {
|
|||||||
margin: "2em 4em",
|
margin: "2em 4em",
|
||||||
padding: "2em",
|
padding: "2em",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Form form={form} onFinish={handleFinish}>
|
<Form form={form} onFinish={handleFinish}>
|
||||||
<ConfigFormComponents componentList={csiquestions} />
|
<ConfigFormComponents componentList={csiquestions} />
|
||||||
<Button
|
<Button
|
||||||
loading={submitting.loading}
|
loading={submitting.loading}
|
||||||
type='primary'
|
type="primary"
|
||||||
htmlType='submit'>
|
htmlType="submit"
|
||||||
|
>
|
||||||
{t("general.actions.submit")}
|
{t("general.actions.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -137,7 +145,7 @@ export default function CsiContainerPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Layout.Footer>
|
<Layout.Footer>
|
||||||
Copyright ImEX.Online. Survey ID: {surveyId}
|
{`Copyright ImEX.Online. Survey ID: ${surveyId}`}
|
||||||
</Layout.Footer>
|
</Layout.Footer>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"intake": "Intake",
|
"intake": "Intake",
|
||||||
"new": "New Appointment",
|
"new": "New Appointment",
|
||||||
"reschedule": "Reschedule",
|
"reschedule": "Reschedule",
|
||||||
|
"smartscheduling": "SMART Scheduling",
|
||||||
"viewjob": "View Job"
|
"viewjob": "View Job"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -30,11 +31,13 @@
|
|||||||
"saving": "Error scheduling appointment. {{message}}"
|
"saving": "Error scheduling appointment. {{message}}"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"time": "Appointment Time",
|
||||||
"title": "Title"
|
"title": "Title"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"arrivedon": "Arrived on: ",
|
"arrivedon": "Arrived on: ",
|
||||||
"cancelledappointment": "Canceled appointment for: ",
|
"cancelledappointment": "Canceled appointment for: ",
|
||||||
|
"history": "History",
|
||||||
"nodateselected": "No date has been selected.",
|
"nodateselected": "No date has been selected.",
|
||||||
"priorappointments": "Previous Appointments",
|
"priorappointments": "Previous Appointments",
|
||||||
"scheduledfor": "Scheduled appointment for: "
|
"scheduledfor": "Scheduled appointment for: "
|
||||||
@@ -76,6 +79,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"address1": "Address 1",
|
"address1": "Address 1",
|
||||||
"address2": "Address 2",
|
"address2": "Address 2",
|
||||||
|
"appt_length": "Default Appointment Length",
|
||||||
"city": "City",
|
"city": "City",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"email": "General Shop Email",
|
"email": "General Shop Email",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"intake": "Consumo",
|
"intake": "Consumo",
|
||||||
"new": "Nueva cita",
|
"new": "Nueva cita",
|
||||||
"reschedule": "Reprogramar",
|
"reschedule": "Reprogramar",
|
||||||
|
"smartscheduling": "",
|
||||||
"viewjob": "Ver trabajo"
|
"viewjob": "Ver trabajo"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -30,11 +31,13 @@
|
|||||||
"saving": "Error al programar la cita. {{message}}"
|
"saving": "Error al programar la cita. {{message}}"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"time": "",
|
||||||
"title": "Título"
|
"title": "Título"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"arrivedon": "Llegado el:",
|
"arrivedon": "Llegado el:",
|
||||||
"cancelledappointment": "Cita cancelada para:",
|
"cancelledappointment": "Cita cancelada para:",
|
||||||
|
"history": "",
|
||||||
"nodateselected": "No se ha seleccionado ninguna fecha.",
|
"nodateselected": "No se ha seleccionado ninguna fecha.",
|
||||||
"priorappointments": "Nombramientos previos",
|
"priorappointments": "Nombramientos previos",
|
||||||
"scheduledfor": "Cita programada para:"
|
"scheduledfor": "Cita programada para:"
|
||||||
@@ -76,6 +79,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"address1": "",
|
"address1": "",
|
||||||
"address2": "",
|
"address2": "",
|
||||||
|
"appt_length": "",
|
||||||
"city": "",
|
"city": "",
|
||||||
"country": "",
|
"country": "",
|
||||||
"email": "",
|
"email": "",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"intake": "Admission",
|
"intake": "Admission",
|
||||||
"new": "Nouveau rendez-vous",
|
"new": "Nouveau rendez-vous",
|
||||||
"reschedule": "Replanifier",
|
"reschedule": "Replanifier",
|
||||||
|
"smartscheduling": "",
|
||||||
"viewjob": "Voir le travail"
|
"viewjob": "Voir le travail"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -30,11 +31,13 @@
|
|||||||
"saving": "Erreur lors de la planification du rendez-vous. {{message}}"
|
"saving": "Erreur lors de la planification du rendez-vous. {{message}}"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"time": "",
|
||||||
"title": "Titre"
|
"title": "Titre"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"arrivedon": "Arrivé le:",
|
"arrivedon": "Arrivé le:",
|
||||||
"cancelledappointment": "Rendez-vous annulé pour:",
|
"cancelledappointment": "Rendez-vous annulé pour:",
|
||||||
|
"history": "",
|
||||||
"nodateselected": "Aucune date n'a été sélectionnée.",
|
"nodateselected": "Aucune date n'a été sélectionnée.",
|
||||||
"priorappointments": "Rendez-vous précédents",
|
"priorappointments": "Rendez-vous précédents",
|
||||||
"scheduledfor": "Rendez-vous prévu pour:"
|
"scheduledfor": "Rendez-vous prévu pour:"
|
||||||
@@ -76,6 +79,7 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"address1": "",
|
"address1": "",
|
||||||
"address2": "",
|
"address2": "",
|
||||||
|
"appt_length": "",
|
||||||
"city": "",
|
"city": "",
|
||||||
"country": "",
|
"country": "",
|
||||||
"email": "",
|
"email": "",
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export const TemplateList = {
|
|||||||
drivingId: "Appointment Id",
|
drivingId: "Appointment Id",
|
||||||
key: "appointment_reminder",
|
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: {
|
parts_order_confirmation: {
|
||||||
title: "Parts Order Confirmation",
|
title: "Parts Order Confirmation",
|
||||||
description: "Parts order template including part details",
|
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
|
- accountingconfig
|
||||||
- address1
|
- address1
|
||||||
- address2
|
- address2
|
||||||
|
- appt_length
|
||||||
- city
|
- city
|
||||||
- country
|
- country
|
||||||
- created_at
|
- created_at
|
||||||
@@ -486,6 +487,7 @@ tables:
|
|||||||
- accountingconfig
|
- accountingconfig
|
||||||
- address1
|
- address1
|
||||||
- address2
|
- address2
|
||||||
|
- appt_length
|
||||||
- city
|
- city
|
||||||
- country
|
- country
|
||||||
- created_at
|
- created_at
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"firebase-admin": "^8.11.0",
|
"firebase-admin": "^8.11.0",
|
||||||
"graphql-request": "^1.8.2",
|
"graphql-request": "^1.8.2",
|
||||||
"handlebars": "^4.7.6",
|
"handlebars": "^4.7.6",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"moment": "^2.26.0",
|
||||||
"nodemailer": "^6.4.4",
|
"nodemailer": "^6.4.4",
|
||||||
"phone": "^2.4.8",
|
"phone": "^2.4.8",
|
||||||
"twilio": "^3.41.1",
|
"twilio": "^3.41.1",
|
||||||
|
|||||||
@@ -1,13 +1,73 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const moment = require("moment");
|
||||||
require("dotenv").config({
|
require("dotenv").config({
|
||||||
path: path.resolve(
|
path: path.resolve(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
`.env.${process.env.NODE_ENV || "development"}`
|
`.env.${process.env.NODE_ENV || "development"}`
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
var _ = require("lodash");
|
||||||
const Handlebars = require("handlebars");
|
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) => {
|
exports.render = (req, res) => {
|
||||||
//Perform request validation
|
//Perform request validation
|
||||||
let view;
|
let view;
|
||||||
|
|||||||
@@ -1992,6 +1992,11 @@ mkdirp@^0.5.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "0.0.8"
|
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:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
|||||||
Reference in New Issue
Block a user