feature/IO-2282-VSSTA-Integration
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -128,3 +128,5 @@ vitest-coverage/
|
|||||||
*.vitest.log
|
*.vitest.log
|
||||||
test-output.txt
|
test-output.txt
|
||||||
server/job/test/fixtures
|
server/job/test/fixtures
|
||||||
|
|
||||||
|
.github
|
||||||
|
|||||||
764
_reference/localEmailViewer/package-lock.json
generated
764
_reference/localEmailViewer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.1",
|
"express": "^5.1.0",
|
||||||
"mailparser": "^3.7.1",
|
"mailparser": "^3.7.2",
|
||||||
"node-fetch": "^3.3.2"
|
"node-fetch": "^3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AlertFilled } from "@ant-design/icons";
|
import { AlertFilled } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
@@ -8,24 +8,30 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||||
|
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||||
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
import ScheduleManualEvent from "../schedule-manual-event/schedule-manual-event.component";
|
||||||
|
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||||
import ScheduleAtChange from "./job-at-change.component";
|
import ScheduleAtChange from "./job-at-change.component";
|
||||||
import ScheduleEventColor from "./schedule-event.color.component";
|
import ScheduleEventColor from "./schedule-event.color.component";
|
||||||
import ScheduleEventNote from "./schedule-event.note.component";
|
import ScheduleEventNote from "./schedule-event.note.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -33,7 +39,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
setMessage: (text) => dispatch(setMessage(text))
|
setMessage: (text) => dispatch(setMessage(text)),
|
||||||
|
insertAuditTrail: ({ jobid, operation }) => dispatch(insertAuditTrail({ jobid, operation }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ScheduleEventComponent({
|
export function ScheduleEventComponent({
|
||||||
@@ -43,16 +50,36 @@ export function ScheduleEventComponent({
|
|||||||
event,
|
event,
|
||||||
refetch,
|
refetch,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
setScheduleContext
|
setScheduleContext,
|
||||||
|
insertAuditTrail
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
const searchParams = queryString.parse(useLocation().search);
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
|
const [mutationUpdateJob] = useMutation(JOB_PRODUCTION_TOGGLE);
|
||||||
const [title, setTitle] = useState(event.title);
|
const [title, setTitle] = useState(event.title);
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||||
|
|
||||||
|
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||||
|
variables: { id: event.job.id },
|
||||||
|
onCompleted: (data) => {
|
||||||
|
if (data?.jobs_by_pk) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
||||||
|
scheduled_completion: data.jobs_by_pk.scheduled_completion,
|
||||||
|
actual_completion: data.jobs_by_pk.actual_completion,
|
||||||
|
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
||||||
|
actual_delivery: data.jobs_by_pk.actual_delivery
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
|
||||||
const blockContent = (
|
const blockContent = (
|
||||||
<Space direction="vertical" wrap>
|
<Space direction="vertical" wrap>
|
||||||
@@ -89,6 +116,74 @@ export function ScheduleEventComponent({
|
|||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleConvert = async (values) => {
|
||||||
|
const res = await mutationUpdateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: event.job.id,
|
||||||
|
job: {
|
||||||
|
...values,
|
||||||
|
status: bodyshop.md_ro_statuses.default_arrived,
|
||||||
|
inproduction: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.errors) {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("jobs.successes.converted")
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: event.job.id,
|
||||||
|
operation: AuditTrailMapping.jobintake(
|
||||||
|
res.data.update_jobs.returning[0].status,
|
||||||
|
DateTimeFormatterFunction(values.scheduled_completion)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
setPopOverVisible(false);
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const popMenu = (
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Form layout="vertical" form={form} onFinish={handleConvert}>
|
||||||
|
<Form.Item
|
||||||
|
name={["actual_in"]}
|
||||||
|
label={t("jobs.fields.actual_in")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={["scheduled_completion"]}
|
||||||
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={["scheduled_delivery"]} label={t("jobs.fields.scheduled_delivery")}>
|
||||||
|
<FormDateTimePickerComponent disabled={event.ro_number} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Space wrap>
|
||||||
|
<Button type="primary" onClick={() => form.submit()}>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const popoverContent = (
|
const popoverContent = (
|
||||||
<div style={{ maxWidth: "40vw" }}>
|
<div style={{ maxWidth: "40vw" }}>
|
||||||
{!event.isintake ? (
|
{!event.isintake ? (
|
||||||
@@ -294,7 +389,7 @@ export function ScheduleEventComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ScheduleManualEvent event={event} />
|
<ScheduleManualEvent event={event} />
|
||||||
)}
|
)}
|
||||||
{event.isintake ? (
|
{event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
pathname: `/manage/jobs/${event.job && event.job.id}/intake`,
|
||||||
@@ -303,7 +398,21 @@ export function ScheduleEventComponent({
|
|||||||
>
|
>
|
||||||
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
<Button disabled={event.arrived}>{t("appointments.actions.intake")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : (
|
||||||
|
<Popover //open={open}
|
||||||
|
content={popMenu}
|
||||||
|
open={popOverVisible}
|
||||||
|
onOpenChange={setPopOverVisible}
|
||||||
|
onClick={(e) => {
|
||||||
|
getJobDetails();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
getPopupContainer={(trigger) => trigger.parentNode}
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<Button disabled={event.arrived}>{t("jobs.actions.intake_quick")}</Button>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
import { GET_JOB_BY_PK_QUICK_INTAKE, JOB_PRODUCTION_TOGGLE } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
@@ -12,7 +13,6 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
import { DateTimeFormatterFunction } from "../../utils/DateFormatter";
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -46,7 +46,16 @@ export function JobsDetailHeaderActionsToggleProduction({
|
|||||||
if (data?.jobs_by_pk) {
|
if (data?.jobs_by_pk) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
actual_in: data.jobs_by_pk.actual_in ? data.jobs_by_pk.actual_in : dayjs(),
|
||||||
scheduled_completion: data.jobs_by_pk.scheduled_completion,
|
scheduled_completion: data.jobs_by_pk.scheduled_completion
|
||||||
|
? data.jobs_by_pk.scheduled_completion
|
||||||
|
: data.jobs_by_pk.labhrs &&
|
||||||
|
data.jobs_by_pk.larhrs &&
|
||||||
|
dayjs().businessDaysAdd(
|
||||||
|
(data.jobs_by_pk.labhrs.aggregate.sum.mod_lb_hrs ||
|
||||||
|
0 + data.jobs_by_pk.larhrs.aggregate.sum.mod_lb_hrs ||
|
||||||
|
0) / bodyshop.target_touchtime,
|
||||||
|
"day"
|
||||||
|
),
|
||||||
actual_completion: data.jobs_by_pk.actual_completion,
|
actual_completion: data.jobs_by_pk.actual_completion,
|
||||||
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
scheduled_delivery: data.jobs_by_pk.scheduled_delivery,
|
||||||
actual_delivery: data.jobs_by_pk.actual_delivery
|
actual_delivery: data.jobs_by_pk.actual_delivery
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export function ProfileShopsContainer({ bodyshop, currentUser }) {
|
|||||||
|
|
||||||
//Force window refresh.
|
//Force window refresh.
|
||||||
|
|
||||||
|
//Ping the new partner to refresh.
|
||||||
|
axios.post("http://localhost:1337/refresh");
|
||||||
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -906,6 +906,7 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
|||||||
add();
|
add();
|
||||||
}}
|
}}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
|
id="insurancecos-add-button"
|
||||||
>
|
>
|
||||||
{t("general.actions.add")}
|
{t("general.actions.add")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2570,6 +2570,20 @@ export const GET_JOB_BY_PK_QUICK_INTAKE = gql`
|
|||||||
actual_completion
|
actual_completion
|
||||||
scheduled_delivery
|
scheduled_delivery
|
||||||
actual_delivery
|
actual_delivery
|
||||||
|
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
larhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||||
|
aggregate {
|
||||||
|
sum {
|
||||||
|
mod_lb_hrs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
import FingerprintJS from "@fingerprintjs/fingerprintjs";
|
||||||
import * as Sentry from "@sentry/browser";
|
|
||||||
import { notification } from "antd";
|
|
||||||
import axios from "axios";
|
|
||||||
import { setUserId, setUserProperties } from "@firebase/analytics";
|
import { setUserId, setUserProperties } from "@firebase/analytics";
|
||||||
import {
|
import {
|
||||||
checkActionCode,
|
checkActionCode,
|
||||||
@@ -12,6 +9,9 @@ import {
|
|||||||
} from "@firebase/auth";
|
} from "@firebase/auth";
|
||||||
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
|
||||||
import { getToken } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
|
import * as Sentry from "@sentry/browser";
|
||||||
|
import { notification } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
|
||||||
@@ -351,7 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
|||||||
});
|
});
|
||||||
payload.features?.allAccess === true
|
payload.features?.allAccess === true
|
||||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||||
: window.$crisp.push(["set", "session:segments", [["basic"]]]);
|
: (() => {
|
||||||
|
const featureKeys = Object.keys(payload.features).filter(
|
||||||
|
(key) =>
|
||||||
|
payload.features[key] === true ||
|
||||||
|
(typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
|
||||||
|
);
|
||||||
|
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
||||||
|
})();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Couldnt find $crisp.", error.message);
|
console.warn("Couldnt find $crisp.", error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const AuditTrailMapping = {
|
|||||||
jobchecklist: (type, inproduction, status) =>
|
jobchecklist: (type, inproduction, status) =>
|
||||||
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }),
|
||||||
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
jobconverted: (ro_number) => i18n.t("audit_trail.messages.jobconverted", { ro_number }),
|
||||||
jobintake: (status, email, scheduled_completion) =>
|
jobintake: (status, scheduled_completion) =>
|
||||||
i18n.t("audit_trail.messages.jobintake", { status, email, scheduled_completion }),
|
i18n.t("audit_trail.messages.jobintake", { status, scheduled_completion }),
|
||||||
jobdelivery: (status, email, actual_completion) =>
|
jobdelivery: (status, email, actual_completion) =>
|
||||||
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
i18n.t("audit_trail.messages.jobdelivery", { status, email, actual_completion }),
|
||||||
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
|
||||||
|
|||||||
@@ -31,6 +31,15 @@
|
|||||||
headers:
|
headers:
|
||||||
- name: x-imex-auth
|
- name: x-imex-auth
|
||||||
value_from_env: DATAPUMP_AUTH
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
- name: Podium Data Pump
|
||||||
|
webhook: '{{HASURA_API_URL}}/data/podium'
|
||||||
|
schedule: 15 5 * * *
|
||||||
|
include_in_metadata: true
|
||||||
|
payload: {}
|
||||||
|
headers:
|
||||||
|
- name: x-imex-auth
|
||||||
|
value_from_env: DATAPUMP_AUTH
|
||||||
|
comment: ""
|
||||||
- name: Rome Usage Report
|
- name: Rome Usage Report
|
||||||
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
webhook: '{{HASURA_API_URL}}/data/usagereport'
|
||||||
schedule: 0 12 * * 5
|
schedule: 0 12 * * 5
|
||||||
|
|||||||
@@ -1005,6 +1005,7 @@
|
|||||||
- pbs_configuration
|
- pbs_configuration
|
||||||
- pbs_serialnumber
|
- pbs_serialnumber
|
||||||
- phone
|
- phone
|
||||||
|
- podiumid
|
||||||
- prodtargethrs
|
- prodtargethrs
|
||||||
- production_config
|
- production_config
|
||||||
- region_config
|
- region_config
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- alter table "public"."bodyshops" add column "podiumid" text
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "podiumid" text
|
||||||
|
null;
|
||||||
@@ -2,7 +2,6 @@ const path = require("path");
|
|||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const moment = require("moment-timezone");
|
const moment = require("moment-timezone");
|
||||||
const converter = require("json-2-csv");
|
const converter = require("json-2-csv");
|
||||||
const _ = require("lodash");
|
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ exports.autohouse = require("./autohouse").default;
|
|||||||
exports.chatter = require("./chatter").default;
|
exports.chatter = require("./chatter").default;
|
||||||
exports.claimscorp = require("./claimscorp").default;
|
exports.claimscorp = require("./claimscorp").default;
|
||||||
exports.kaizen = require("./kaizen").default;
|
exports.kaizen = require("./kaizen").default;
|
||||||
exports.usageReport = require("./usageReport").default;
|
exports.usageReport = require("./usageReport").default;
|
||||||
|
exports.podium = require("./podium").default;
|
||||||
211
server/data/podium.js
Normal file
211
server/data/podium.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
const moment = require("moment-timezone");
|
||||||
|
const converter = require("json-2-csv");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const fs = require("fs");
|
||||||
|
require("dotenv").config({
|
||||||
|
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||||
|
});
|
||||||
|
let Client = require("ssh2-sftp-client");
|
||||||
|
|
||||||
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
const { sendServerEmail } = require("../email/sendemail");
|
||||||
|
|
||||||
|
const ftpSetup = {
|
||||||
|
host: process.env.PODIUM_HOST,
|
||||||
|
port: process.env.PODIUM_PORT,
|
||||||
|
username: process.env.PODIUM_USER,
|
||||||
|
password: process.env.PODIUM_PASSWORD,
|
||||||
|
debug:
|
||||||
|
process.env.NODE_ENV !== "production"
|
||||||
|
? (message, ...data) => logger.log(message, "DEBUG", "api", null, data)
|
||||||
|
: () => {},
|
||||||
|
algorithms: {
|
||||||
|
serverHostKey: ["ssh-rsa", "ssh-dss", "rsa-sha2-256", "rsa-sha2-512", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.default = async (req, res) => {
|
||||||
|
// Only process if in production environment.
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
res.sendStatus(403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only process if the appropriate token is provided.
|
||||||
|
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||||
|
res.sendStatus(401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send immediate response and continue processing.
|
||||||
|
res.status(202).json({
|
||||||
|
success: true,
|
||||||
|
message: "Processing request ...",
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.log("podium-start", "DEBUG", "api", null, null);
|
||||||
|
const allCSVResults = [];
|
||||||
|
const allErrors = [];
|
||||||
|
|
||||||
|
const { bodyshops } = await client.request(queries.GET_PODIUM_SHOPS); //Query for the List of Bodyshop Clients.
|
||||||
|
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
||||||
|
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
||||||
|
|
||||||
|
const shopsToProcess =
|
||||||
|
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||||
|
logger.log("podium-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||||
|
|
||||||
|
if (shopsToProcess.length === 0) {
|
||||||
|
logger.log("podium-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await processShopData(shopsToProcess, start, end, skipUpload, allCSVResults, allErrors);
|
||||||
|
|
||||||
|
await sendServerEmail({
|
||||||
|
subject: `Podium Report ${moment().format("MM-DD-YY")}`,
|
||||||
|
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||||
|
allCSVResults.map((x) => ({
|
||||||
|
imexshopid: x.imexshopid,
|
||||||
|
filename: x.filename,
|
||||||
|
count: x.count,
|
||||||
|
result: x.result
|
||||||
|
})),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("podium-end", "DEBUG", "api", null, null);
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("podium-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function processShopData(shopsToProcess, start, end, skipUpload, allCSVResults, allErrors) {
|
||||||
|
for (const bodyshop of shopsToProcess) {
|
||||||
|
const erroredJobs = [];
|
||||||
|
try {
|
||||||
|
logger.log("podium-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
|
shopname: bodyshop.shopname
|
||||||
|
});
|
||||||
|
|
||||||
|
const { jobs, bodyshops_by_pk } = await client.request(queries.PODIUM_QUERY, {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
start: start ? moment(start).startOf("day") : moment().subtract(2, "days").startOf("day"),
|
||||||
|
...(end && { end: moment(end).endOf("day") })
|
||||||
|
});
|
||||||
|
|
||||||
|
const podiumObject = jobs.map((j) => {
|
||||||
|
return {
|
||||||
|
"Podium Account ID": bodyshops_by_pk.podiumid,
|
||||||
|
"First Name": j.ownr_co_nm ? null : j.ownr_fn,
|
||||||
|
"Last Name": j.ownr_co_nm ? j.ownr_co_nm : j.ownr_ln,
|
||||||
|
"SMS Number": null,
|
||||||
|
"Phone 1": j.ownr_ph1,
|
||||||
|
"Phone 2": j.ownr_ph2,
|
||||||
|
Email: j.ownr_ea,
|
||||||
|
"Delivered Date":
|
||||||
|
(j.actual_delivery && moment(j.actual_delivery).tz(bodyshop.timezone).format("MM/DD/YYYY")) || ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (erroredJobs.length > 0) {
|
||||||
|
logger.log("podium-failed-jobs", "ERROR", "api", bodyshop.id, {
|
||||||
|
count: erroredJobs.length,
|
||||||
|
jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvObj = {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
csv: converter.json2csv(podiumObject, { emptyFieldValue: "" }),
|
||||||
|
filename: `${bodyshop.podiumid}-${moment().format("YYYYMMDDTHHMMss")}.csv`,
|
||||||
|
count: podiumObject.length
|
||||||
|
};
|
||||||
|
|
||||||
|
if (skipUpload) {
|
||||||
|
fs.writeFileSync(`./logs/${csvObj.filename}`, csvObj.csv);
|
||||||
|
} else {
|
||||||
|
await uploadViaSFTP(csvObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
allCSVResults.push({
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
podiumid: bodyshop.podiumid,
|
||||||
|
count: csvObj.count,
|
||||||
|
filename: csvObj.filename,
|
||||||
|
result: csvObj.result
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("podium-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||||
|
shopname: bodyshop.shopname
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
//Error at the shop level.
|
||||||
|
logger.log("podium-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
|
||||||
|
|
||||||
|
allErrors.push({
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
podiumid: bodyshop.podiumid,
|
||||||
|
fatal: true,
|
||||||
|
errors: [error.toString()]
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
allErrors.push({
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
podiumid: bodyshop.podiumid,
|
||||||
|
errors: erroredJobs.map((ej) => ({
|
||||||
|
ro_number: ej.job?.ro_number,
|
||||||
|
jobid: ej.job?.id,
|
||||||
|
error: ej.error
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadViaSFTP(csvObj) {
|
||||||
|
const sftp = new Client();
|
||||||
|
sftp.on("error", (errors) =>
|
||||||
|
logger.log("podium-sftp-connection-error", "ERROR", "api", csvObj.bodyshopid, {
|
||||||
|
error: errors.message,
|
||||||
|
stack: errors.stack
|
||||||
|
})
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
//Connect to the FTP and upload all.
|
||||||
|
await sftp.connect(ftpSetup);
|
||||||
|
|
||||||
|
try {
|
||||||
|
csvObj.result = await sftp.put(Buffer.from(csvObj.xml), `${csvObj.filename}`);
|
||||||
|
logger.log("podium-sftp-upload", "DEBUG", "api", csvObj.bodyshopid, {
|
||||||
|
imexshopid: csvObj.imexshopid,
|
||||||
|
filename: csvObj.filename,
|
||||||
|
result: csvObj.result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("podium-sftp-upload-error", "ERROR", "api", csvObj.bodyshopid, {
|
||||||
|
filename: csvObj.filename,
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("podium-sftp-error", "ERROR", "api", csvObj.bodyshopid, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
sftp.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
const moment = require("moment");
|
|
||||||
const { default: RenderInstanceManager } = require("../utils/instanceMgr");
|
|
||||||
const { header, end, start } = require("./html");
|
const { header, end, start } = require("./html");
|
||||||
|
|
||||||
// Required Strings
|
// Required Strings
|
||||||
@@ -7,19 +5,6 @@ const { header, end, start } = require("./html");
|
|||||||
// - subHeader - The subheader of the email
|
// - subHeader - The subheader of the email
|
||||||
// - body - The body of the email
|
// - body - The body of the email
|
||||||
|
|
||||||
// Optional Strings (Have default values)
|
|
||||||
// - footer - The footer of the email
|
|
||||||
// - dateLine - The date line of the email
|
|
||||||
|
|
||||||
const defaultFooter = () => {
|
|
||||||
return RenderInstanceManager({
|
|
||||||
imex: "ImEX Online Collision Repair Management System",
|
|
||||||
rome: "Rome Technologies"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const now = () => moment().format("MM/DD/YYYY @ hh:mm a");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the email template
|
* Generate the email template
|
||||||
* @param strings
|
* @param strings
|
||||||
@@ -32,81 +17,48 @@ const generateEmailTemplate = (strings) => {
|
|||||||
header +
|
header +
|
||||||
start +
|
start +
|
||||||
`
|
`
|
||||||
<table class="row">
|
<!-- Report Title -->
|
||||||
<tbody>
|
${
|
||||||
<tr>
|
strings.header &&
|
||||||
<th class="small-12 large-12 columns first last">
|
`
|
||||||
<table>
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
<tbody>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 8px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
<tr>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
<td>
|
<h6 style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; color: inherit; word-wrap: normal; font-weight: normal; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 23px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; text-align: center;"><strong style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">${strings.header}</strong></h6>
|
||||||
<h6 style="text-align:left"><strong>${strings.header}</strong></h6>
|
</td></tr>
|
||||||
</td>
|
</tbody></table></th>
|
||||||
</tr>
|
</tr></tbody></table>
|
||||||
<tr>
|
`
|
||||||
<td>
|
}
|
||||||
<p style="font-size:90%">${strings.subHeader}</p>
|
${
|
||||||
</td>
|
strings.subHeader &&
|
||||||
</tr>
|
`
|
||||||
</tbody>
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
</table>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
</th>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
</tr>
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 95%;">${strings.subHeader}</p>
|
||||||
</tbody>
|
</td></tr>
|
||||||
</table>
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
`
|
||||||
|
}
|
||||||
<!-- End Report Title -->
|
<!-- End Report Title -->
|
||||||
<!-- Task Detail -->
|
${
|
||||||
<table class="row">
|
strings.body &&
|
||||||
<tbody>
|
`
|
||||||
<tr>
|
<!-- Report Detail -->
|
||||||
<th class="small-12 large-12 columns first last">
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
<table>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
<tbody>
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
<tr>
|
${strings.body}
|
||||||
<td>${strings.body}</td>
|
</td></tr>
|
||||||
</tr>
|
</tbody></table></th>
|
||||||
</tbody>
|
</tr></tbody></table>
|
||||||
</table>
|
<!-- End Report Detail -->
|
||||||
</th>
|
`
|
||||||
</tr>
|
}
|
||||||
</tbody>
|
` +
|
||||||
</table>
|
end(strings.dateLine)
|
||||||
<!-- End Task Detail -->
|
|
||||||
<!-- Footer -->
|
|
||||||
<table class="row collapsed footer" id="non-printable">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th class="small-3 large-3 columns first">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%; padding-right:10px">${strings?.dateLine || now()}</p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
<th class="small-6 large-6 columns">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%; text-align:center">${strings?.footer || defaultFooter()}</p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
<th class="small-3 large-3 columns last">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><p style="font-size:70%"> </p></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>` +
|
|
||||||
end
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
2765
server/email/html.js
2765
server/email/html.js
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,9 @@ const logEmail = async (req, email) => {
|
|||||||
to: req?.body?.to,
|
to: req?.body?.to,
|
||||||
cc: req?.body?.cc,
|
cc: req?.body?.cc,
|
||||||
subject: req?.body?.subject,
|
subject: req?.body?.subject,
|
||||||
email
|
email,
|
||||||
|
errorMessage: error?.message,
|
||||||
|
errorStack: error?.stack
|
||||||
// info,
|
// info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,7 @@ const sendServerEmail = async ({ subject, text }) => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
(err, info) => {
|
(err, info) => {
|
||||||
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
||||||
message: err?.message,
|
message: err?.message,
|
||||||
@@ -80,6 +83,108 @@ const sendServerEmail = async ({ subject, text }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendWelcomeEmail = async ({ to, resetLink, dateLine, features, bcc }) => {
|
||||||
|
try {
|
||||||
|
await mailer.sendMail({
|
||||||
|
from: InstanceManager({
|
||||||
|
imex: `ImEX Online <noreply@imex.online>`,
|
||||||
|
rome: `Rome Online <noreply@romeonline.io>`
|
||||||
|
}),
|
||||||
|
to,
|
||||||
|
bcc,
|
||||||
|
subject: InstanceManager({
|
||||||
|
imex: "Welcome to the ImEX Online platform.",
|
||||||
|
rome: "Welcome to the Rome Online platform."
|
||||||
|
}),
|
||||||
|
html: generateEmailTemplate({
|
||||||
|
header: InstanceManager({
|
||||||
|
imex: "Welcome to the ImEX Online platform.",
|
||||||
|
rome: "Welcome to the Rome Online platform."
|
||||||
|
}),
|
||||||
|
subHeader: `Your ${InstanceManager({imex: features?.allAccess ? "ImEX Online": "ImEX Lite", rome: features?.allAccess ? "RO Manager" : "RO Basic"})} shop setup has been completed, and this email will include all the information you need to begin.`,
|
||||||
|
body: `
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">To finish setting up your account, visit this link and enter your desired password. <a href=${resetLink} style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Reset Password</a></p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">To access your ${InstanceManager({imex: features.allAccess ? "ImEX Online": "ImEX Lite", rome: features.allAccess ? "RO Manager" : "RO Basic"})} shop, visit <a href=${InstanceManager({imex: "https://imex.online/", rome: "https://romeonline.io/"})} style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">${InstanceManager({imex: "imex.online", rome: "romeonline.io"})}</a>. Your username is your email, and your password is what you previously set up. Contact support for additional logins.</p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
${InstanceManager({
|
||||||
|
rome: `
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">To push estimates over from your estimating system, you must download the Web-Est EMS Unzipper & Rome Online Partner (Computers using Windows only). Here are some steps to help you get started.</p>
|
||||||
|
</td><tr>
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 1%; padding-left: 30px;">
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Download and set up the Web-Est EMS Unzipper - <a href="https://help.imex.online/en/article/how-to-set-up-the-ems-unzip-downloader-on-web-est-n9hbcv/" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">How to setup the EMS Unzip Downloader on Web-Est</a></li>
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Download and set up Rome Online Partner - <a href="https://help.imex.online/en/article/setting-up-the-rome-online-partner-1xsw8tb/" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Setting up the Rome Online Partner</a></li>
|
||||||
|
</ul>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">Once you successfully set up the partner, now it's time to do some initial in-product items: Please note, <b>an estimate must be exported from the estimating platform to use tours.</b></p>
|
||||||
|
</td><tr>
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 1%; padding-left: 30px;">
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Send estimate from Web-Est to RO Basic - <a href="https://help.imex.online/en/article/how-to-send-estimates-from-web-est-to-the-management-system-ox0h9a/" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">How to setup the EMS Unzip Downloader on Web-Est</a></li>
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">Once completed, learn how to use RO Basic by accessing the tours at the bottom middle of the screen (labeled “Training Tours”). These walkthroughs will show you how to navigate from creating an RO to closing an RO - <a href="https://www.youtube.com/watch?v=gcbSe5med0I" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">ROME Collision Management Youtube Training Videos</a></li>
|
||||||
|
</ul>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">If you need any assistance with setting up the programs, or if you want a dedicated Q&A session with one of our customer success specialists, schedule by clicking this link - <a href="https://rometech.zohobookings.com/#/PSAT" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Rome Basic Training Booking</a></p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">If you have additional questions or need any support, feel free to use the RO Basic support chat (blue chat box located in the bottom right corner) or give us a call at <a href="tel:14103576700" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">(410) 357-6700</a>. We are here to help make your experience seamless!</p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
`
|
||||||
|
})}
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">In addition to the training tour, you can also book a live one-on-one demo to see exactly how our system can help streamline the repair process at your shop, schedule by clicking this link - <a href="https://outlook.office.com/bookwithme/user/0aa3ae2c6d59497d9f93fb72479848dc@imexsystems.ca/meetingtype/Qy7CsXl5MkuUJ0NRD7B1AA2?anonymous&ep=mlink" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">${InstanceManager({imex: "ImEX Lite", rome: "Rome Basic"})} Demo Booking</a></p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 8px; width: 734px; padding-left: 0px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">Thanks,</p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table></th>
|
||||||
|
</tr></tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 8px; width: 734px; padding-left: 0px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;">
|
||||||
|
<tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">The ${InstanceManager({imex: "ImEX Online", rome: "Rome Online"})} Team</p>
|
||||||
|
`,
|
||||||
|
dateLine
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("server-email-failure", "error", null, null, { error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => {
|
const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachments }) => {
|
||||||
try {
|
try {
|
||||||
mailer.sendMail(
|
mailer.sendMail(
|
||||||
@@ -93,6 +198,7 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
|
|||||||
...(type === "text" ? { text } : { html }),
|
...(type === "text" ? { text } : { html }),
|
||||||
attachments: attachments || null
|
attachments: attachments || null
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
(err, info) => {
|
(err, info) => {
|
||||||
// (message, type, user, record, meta
|
// (message, type, user, record, meta
|
||||||
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
|
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
|
||||||
@@ -143,22 +249,20 @@ const sendEmail = async (req, res) => {
|
|||||||
to: req.body.to,
|
to: req.body.to,
|
||||||
cc: req.body.cc,
|
cc: req.body.cc,
|
||||||
subject: req.body.subject,
|
subject: req.body.subject,
|
||||||
attachments:
|
attachments: [
|
||||||
[
|
...(req.body.attachments &&
|
||||||
...((req.body.attachments &&
|
req.body.attachments.map((a) => {
|
||||||
req.body.attachments.map((a) => {
|
|
||||||
return {
|
|
||||||
filename: a.filename,
|
|
||||||
path: a.path
|
|
||||||
};
|
|
||||||
})) ||
|
|
||||||
[]),
|
|
||||||
...downloadedMedia.map((a) => {
|
|
||||||
return {
|
return {
|
||||||
path: a
|
filename: a.filename,
|
||||||
|
path: a.path
|
||||||
};
|
};
|
||||||
})
|
})),
|
||||||
] || null,
|
...downloadedMedia.map((a) => {
|
||||||
|
return {
|
||||||
|
path: a
|
||||||
|
};
|
||||||
|
})
|
||||||
|
],
|
||||||
html: isObject(req.body?.templateStrings) ? generateEmailTemplate(req.body.templateStrings) : req.body.html,
|
html: isObject(req.body?.templateStrings) ? generateEmailTemplate(req.body.templateStrings) : req.body.html,
|
||||||
ses: {
|
ses: {
|
||||||
// optional extra arguments for SendRawEmail
|
// optional extra arguments for SendRawEmail
|
||||||
@@ -273,6 +377,7 @@ ${body.bounce?.bouncedRecipients.map(
|
|||||||
)}
|
)}
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
(err, info) => {
|
(err, info) => {
|
||||||
logger.log("sns-error", err ? "error" : "debug", "api", null, {
|
logger.log("sns-error", err ? "error" : "debug", "api", null, {
|
||||||
errorMessage: err?.message,
|
errorMessage: err?.message,
|
||||||
@@ -294,5 +399,6 @@ module.exports = {
|
|||||||
sendEmail,
|
sendEmail,
|
||||||
sendServerEmail,
|
sendServerEmail,
|
||||||
sendTaskEmail,
|
sendTaskEmail,
|
||||||
emailBounce
|
emailBounce,
|
||||||
|
sendWelcomeEmail
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ const { formatTaskPriority } = require("../notifications/stringHelpers");
|
|||||||
const tasksEmailQueue = taskEmailQueue();
|
const tasksEmailQueue = taskEmailQueue();
|
||||||
|
|
||||||
// Cleanup function for the Tasks Email Queue
|
// Cleanup function for the Tasks Email Queue
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const tasksEmailQueueCleanup = async () => {
|
const tasksEmailQueueCleanup = async () => {
|
||||||
try {
|
try {
|
||||||
// Example async operation
|
// Example async operation
|
||||||
// console.log("Performing Tasks Email Reminder process cleanup...");
|
// console.log("Performing Tasks Email Reminder process cleanup...");
|
||||||
await new Promise((resolve) => tasksEmailQueue.destroy(() => resolve()));
|
await new Promise((resolve) => tasksEmailQueue.destroy(() => resolve()));
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// console.error("Tasks Email Reminder process cleanup failed:", err);
|
// console.error("Tasks Email Reminder process cleanup failed:", err);
|
||||||
}
|
}
|
||||||
@@ -254,10 +256,15 @@ const tasksRemindEmail = async (req, res) => {
|
|||||||
header: `${allTasks.length} Tasks require your attention`,
|
header: `${allTasks.length} Tasks require your attention`,
|
||||||
subHeader: `Please click on the Tasks below to view the Task.`,
|
subHeader: `Please click on the Tasks below to view the Task.`,
|
||||||
dateLine,
|
dateLine,
|
||||||
body: `<ul>
|
body: `
|
||||||
|
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; margin: 1%; padding-left: 30px;">
|
||||||
${allTasks
|
${allTasks
|
||||||
.map((task) =>
|
.map((task) =>
|
||||||
`<li><a href="${InstanceEndpoints()}/manage/tasks/alltasks?taskid=${task.id}">${task.title} - Priority: ${formatTaskPriority(task.priority)} ${task.due_date ? `${formatDate(task.due_date)}` : ""} | Bodyshop: ${task.bodyshop.shopname}</a></li>`.trim()
|
`
|
||||||
|
<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">
|
||||||
|
<a href="${InstanceEndpoints()}/manage/tasks/alltasks?taskid=${task.id}" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">${task.title} - Priority: ${formatTaskPriority(task.priority)} ${task.due_date ? `${formatDate(task.due_date)}` : ""} | Bodyshop: ${task.bodyshop.shopname}</a>
|
||||||
|
</li>
|
||||||
|
`.trim()
|
||||||
)
|
)
|
||||||
.join("")}
|
.join("")}
|
||||||
</ul>`
|
</ul>`
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
const admin = require("firebase-admin");
|
|
||||||
const logger = require("../utils/logger");
|
|
||||||
//const { sendProManagerWelcomeEmail } = require("../email/sendemail");
|
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
|
||||||
const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
|
const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
|
||||||
//const generateEmailTemplate = require("../email/generateTemplate");
|
const admin = require("firebase-admin");
|
||||||
|
const moment = require("moment-timezone");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
const { sendWelcomeEmail } = require("../email/sendemail");
|
||||||
|
const { GET_USER_BY_EMAIL } = require("../graphql-client/queries");
|
||||||
|
|
||||||
admin.initializeApp({
|
admin.initializeApp({
|
||||||
credential: admin.credential.cert(serviceAccount),
|
credential: admin.credential.cert(serviceAccount),
|
||||||
@@ -201,6 +197,94 @@ const unsubscribe = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getWelcomeEmail = async (req, res) => {
|
||||||
|
const { authid, email, bcc } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch user from Firebase
|
||||||
|
const userRecord = await admin.auth().getUser(authid);
|
||||||
|
if (!userRecord) {
|
||||||
|
throw { status: 404, message: "User not found in Firebase." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user data from the database using GraphQL
|
||||||
|
const dbUserResult = await client.request(GET_USER_BY_EMAIL, { email: email.toLowerCase() });
|
||||||
|
|
||||||
|
const dbUser = dbUserResult?.users?.[0];
|
||||||
|
if (!dbUser) {
|
||||||
|
throw { status: 404, message: "User not found in database." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate email before proceeding
|
||||||
|
if (!dbUser.validemail) {
|
||||||
|
logger.log("admin-send-welcome-email-skip", "debug", req.user.email, null, {
|
||||||
|
message: "User email is not valid, skipping email.",
|
||||||
|
email
|
||||||
|
});
|
||||||
|
return res.status(200).json({ message: "User email is not valid, email not sent." });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate password reset link
|
||||||
|
const resetLink = await admin.auth().generatePasswordResetLink(dbUser.email);
|
||||||
|
|
||||||
|
// Send welcome email
|
||||||
|
await sendWelcomeEmail({
|
||||||
|
to: dbUser.email,
|
||||||
|
resetLink,
|
||||||
|
dateLine: moment().tz(dbUser.associations?.[0]?.bodyshop?.timezone).format("MM/DD/YYYY @ hh:mm a"),
|
||||||
|
features: dbUser.associations?.[0]?.bodyshop?.features,
|
||||||
|
bcc
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log success and return response
|
||||||
|
logger.log("admin-send-welcome-email", "debug", req.user.email, null, {
|
||||||
|
request: req.body,
|
||||||
|
ioadmin: true,
|
||||||
|
emailSentTo: email
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Welcome email sent successfully." });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("admin-send-welcome-email-error", "ERROR", req.user.email, null, { error });
|
||||||
|
|
||||||
|
if (!res.headersSent) {
|
||||||
|
return res.status(error.status || 500).json({
|
||||||
|
message: error.message || "Error sending welcome email.",
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getResetLink = async (req, res) => {
|
||||||
|
const { authid, email } = req.body;
|
||||||
|
logger.log("admin-reset-link", "debug", req.user.email, null, { authid, email });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch user from Firebase
|
||||||
|
const userRecord = await admin.auth().getUser(authid);
|
||||||
|
if (!userRecord) {
|
||||||
|
throw { status: 404, message: "User not found in Firebase." };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate password reset link
|
||||||
|
const resetLink = await admin.auth().generatePasswordResetLink(email);
|
||||||
|
|
||||||
|
// Log success and return response
|
||||||
|
logger.log("admin-reset-link-success", "debug", req.user.email, null, {
|
||||||
|
request: req.body,
|
||||||
|
ioadmin: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({ message: "Reset link generated successfully.", resetLink });
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(error.status || 500).json({
|
||||||
|
message: error.message || "Error generating reset link.",
|
||||||
|
error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
admin,
|
admin,
|
||||||
createUser,
|
createUser,
|
||||||
@@ -208,23 +292,7 @@ module.exports = {
|
|||||||
getUser,
|
getUser,
|
||||||
sendNotification,
|
sendNotification,
|
||||||
subscribe,
|
subscribe,
|
||||||
unsubscribe
|
unsubscribe,
|
||||||
|
getWelcomeEmail,
|
||||||
|
getResetLink
|
||||||
};
|
};
|
||||||
|
|
||||||
//Admin claims code.
|
|
||||||
// const uid = "JEqqYlsadwPEXIiyRBR55fflfko1";
|
|
||||||
|
|
||||||
// admin
|
|
||||||
// .auth()
|
|
||||||
// .getUser(uid)
|
|
||||||
// .then((user) => {
|
|
||||||
// console.log(user);
|
|
||||||
// admin.auth().setCustomUserClaims(uid, {
|
|
||||||
// ioadmin: true,
|
|
||||||
// "https://hasura.io/jwt/claims": {
|
|
||||||
// "x-hasura-default-role": "debug",
|
|
||||||
// "x-hasura-allowed-roles": ["admin"],
|
|
||||||
// "x-hasura-user-id": uid,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|||||||
@@ -1323,6 +1323,27 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
exports.PODIUM_QUERY = `query PODIUM_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
|
||||||
|
bodyshops_by_pk(id: $bodyshopid){
|
||||||
|
id
|
||||||
|
shopname
|
||||||
|
podiumid
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
jobs(where: {_and: [{converted: {_eq: true}}, {actual_delivery: {_gt: $start}}, {actual_delivery: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}, {_or: [{ownr_ph1: {_is_null: false}}, {ownr_ea: {_is_null: false}}]}]}) {
|
||||||
|
actual_delivery
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
ro_number
|
||||||
|
ownr_fn
|
||||||
|
ownr_ln
|
||||||
|
ownr_co_nm
|
||||||
|
ownr_ph1
|
||||||
|
ownr_ph2
|
||||||
|
ownr_ea
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
exports.UPDATE_JOB = `
|
exports.UPDATE_JOB = `
|
||||||
mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) {
|
mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) {
|
||||||
update_jobs(where: { id: { _eq: $jobId } }, _set: $job) {
|
update_jobs(where: { id: { _eq: $jobId } }, _set: $job) {
|
||||||
@@ -1848,6 +1869,16 @@ exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($imexshopid: [String]) {
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
exports.GET_PODIUM_SHOPS = `query GET_PODIUM_SHOPS {
|
||||||
|
bodyshops(where: {podiumid: {_is_null: false}, _or: {podiumid: {_neq: ""}}}){
|
||||||
|
id
|
||||||
|
shopname
|
||||||
|
podiumid
|
||||||
|
imexshopid
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{
|
exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{
|
||||||
delete_dms_vehicles(where: {}) {
|
delete_dms_vehicles(where: {}) {
|
||||||
affected_rows
|
affected_rows
|
||||||
@@ -2854,6 +2885,24 @@ query GET_BODYSHOP_BY_MERCHANTID($merchantID: String!) {
|
|||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
exports.GET_USER_BY_EMAIL = `
|
||||||
|
query GET_USER_BY_EMAIL($email: String!) {
|
||||||
|
users(where: {email: {_eq: $email}}) {
|
||||||
|
email
|
||||||
|
validemail
|
||||||
|
associations {
|
||||||
|
id
|
||||||
|
shopid
|
||||||
|
bodyshop {
|
||||||
|
id
|
||||||
|
convenient_company
|
||||||
|
features
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
// Define the GraphQL query to get a job by RO number and shop ID
|
// Define the GraphQL query to get a job by RO number and shop ID
|
||||||
exports.GET_JOB_BY_RO_NUMBER_AND_SHOP_ID = `
|
exports.GET_JOB_BY_RO_NUMBER_AND_SHOP_ID = `
|
||||||
query GET_JOB_BY_RO_NUMBER_AND_SHOP_ID($roNumber: String!, $shopId: uuid!) {
|
query GET_JOB_BY_RO_NUMBER_AND_SHOP_ID($roNumber: String!, $shopId: uuid!) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { sendTaskEmail } = require("../../email/sendemail");
|
const { sendTaskEmail } = require("../../email/sendemail");
|
||||||
const generateEmailTemplate = require("../../email/generateTemplate");
|
const generateEmailTemplate = require("../../email/generateTemplate");
|
||||||
|
const { InstanceEndpoints } = require("../../utils/instanceMgr");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Send notification email to the user
|
* @description Send notification email to the user
|
||||||
@@ -22,11 +23,9 @@ const sendPaymentNotificationEmail = async (userEmail, jobs, partialPayments, lo
|
|||||||
body: jobs.jobs
|
body: jobs.jobs
|
||||||
.map(
|
.map(
|
||||||
(job) =>
|
(job) =>
|
||||||
`Reference: <a href="${InstanceEndpoints()}/manage/jobs/${job.id}">${job.ro_number || "N/A"}</a> | ${
|
`<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;">Reference: <a href="${InstanceEndpoints()}/manage/jobs/${job.id}" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">${job.ro_number || "N/A"}</a> | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}</p>`
|
||||||
job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()
|
|
||||||
} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()} | $${partialPayments.find((p) => p.jobid === job.id).amount}`
|
|
||||||
)
|
)
|
||||||
.join("<br/>")
|
.join("")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -133,11 +133,19 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
|
|||||||
subHeader: `Dear ${firstName},`,
|
subHeader: `Dear ${firstName},`,
|
||||||
dateLine: moment().tz(timezone).format("MM/DD/YYYY hh:mm a"),
|
dateLine: moment().tz(timezone).format("MM/DD/YYYY hh:mm a"),
|
||||||
body: `
|
body: `
|
||||||
<p>There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p><br/>
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 100%;">There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p>
|
||||||
<ul>
|
</td></tr></table></th>
|
||||||
${messages.map((msg) => `<li>${msg}</li>`).join("")}
|
</tr></tbody></table>
|
||||||
</ul><br/><br/>
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
<p><a href="${InstanceEndpoints()}/manage/jobs/${jobId}">Please check the job for more details.</a></p>
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<ul style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 1%; padding-left: 30px;">
|
||||||
|
${messages.map((msg) => `<li style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%;">${msg}</li>`).join("")}
|
||||||
|
</ul>
|
||||||
|
</td></tr></table></th>
|
||||||
|
</tr><tbody></table>
|
||||||
|
<table class="row" style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; padding: 0; width: 100%; position: relative; display: table;"><tbody style="font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; display: table-row-group;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;">
|
||||||
|
<th class="small-12 large-12 columns first last" style="word-wrap: break-word; -webkit-hyphens: auto; -moz-hyphens: auto; hyphens: auto; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; line-height: 1.2; margin: 0 auto; Margin: 0 auto; padding-bottom: 16px; width: 734px; padding-left: 8px; padding-right: 8px; border-collapse: collapse;"><table style="border-spacing: 0; border-collapse: collapse; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; width: 100%;"><tr style="padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; vertical-align: top; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif;"><td style="word-wrap: break-word; vertical-align: top; color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; margin: 0; Margin: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 15px; word-break: keep-all; -moz-hyphens: none; -ms-hyphens: none; -webkit-hyphens: none; hyphens: none; line-height: 1.2; border-collapse: collapse;">
|
||||||
|
<p style="color: #0a0a0a; font-weight: normal; padding-top: 0; padding-right: 0; padding-bottom: 0; padding-left: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; margin: 0 0 0 0px; Margin: 0 0 0 0px; line-height: 1.2; margin-bottom: 0px; Margin-bottom: 0px; font-size: 90%;"><a href="${InstanceEndpoints()}/manage/jobs/${jobId}" style="color: #2199e8; text-decoration: none; font-weight: normal; padding: 0; text-align: left; font-family: 'Montserrat', 'Montserrat Alternates', sans-serif; font-size: 90%; line-height: 1.2;">Please check the job for more details.</a></p>
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
await sendTaskEmail({
|
await sendTaskEmail({
|
||||||
@@ -226,6 +234,7 @@ const getQueue = () => {
|
|||||||
* @param {Object} options.logger - Logger instance for logging dispatch events.
|
* @param {Object} options.logger - Logger instance for logging dispatch events.
|
||||||
* @returns {Promise<void>} Resolves when all notifications are added to the queue.
|
* @returns {Promise<void>} Resolves when all notifications are added to the queue.
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
|
||||||
const emailAddQueue = getQueue();
|
const emailAddQueue = getQueue();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const express = require("express");
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
const { createAssociation, createShop, updateShop, updateCounter } = require("../admin/adminops");
|
const { createAssociation, createShop, updateShop, updateCounter } = require("../admin/adminops");
|
||||||
const { updateUser, getUser, createUser } = require("../firebase/firebase-handler");
|
const { updateUser, getUser, createUser, getWelcomeEmail, getResetLink } = require("../firebase/firebase-handler");
|
||||||
const validateAdminMiddleware = require("../middleware/validateAdminMiddleware");
|
const validateAdminMiddleware = require("../middleware/validateAdminMiddleware");
|
||||||
|
|
||||||
router.use(validateFirebaseIdTokenMiddleware);
|
router.use(validateFirebaseIdTokenMiddleware);
|
||||||
@@ -15,5 +15,7 @@ router.post("/updatecounter", updateCounter);
|
|||||||
router.post("/updateuser", updateUser);
|
router.post("/updateuser", updateUser);
|
||||||
router.post("/getuser", getUser);
|
router.post("/getuser", getUser);
|
||||||
router.post("/createuser", createUser);
|
router.post("/createuser", createUser);
|
||||||
|
router.post("/sendwelcome", getWelcomeEmail);
|
||||||
|
router.post("/resetlink", getResetLink);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { autohouse, claimscorp, chatter, kaizen, usageReport } = require("../data/data");
|
const { autohouse, claimscorp, chatter, kaizen, usageReport, podium } = require("../data/data");
|
||||||
|
|
||||||
router.post("/ah", autohouse);
|
router.post("/ah", autohouse);
|
||||||
router.post("/cc", claimscorp);
|
router.post("/cc", claimscorp);
|
||||||
router.post("/chatter", chatter);
|
router.post("/chatter", chatter);
|
||||||
router.post("/kaizen", kaizen);
|
router.post("/kaizen", kaizen);
|
||||||
router.post("/usagereport", usageReport);
|
router.post("/usagereport", usageReport);
|
||||||
|
router.post("/podium", podium);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user