-
- {/* Outbound message metadata */}
- {message.isoutbound && (
+ {/* Outbound message metadata for non-system messages */}
+ {!isSystem && message.isoutbound && (
{i18n.t("messaging.labels.sentby", {
by: message.userid,
diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx
index 824d5e591..5b4d31d14 100644
--- a/client/src/components/chat-send-message/chat-send-message.component.jsx
+++ b/client/src/components/chat-send-message/chat-send-message.component.jsx
@@ -1,6 +1,6 @@
-import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
-import { Input, Spin } from "antd";
-import React, { useEffect, useRef, useState } from "react";
+import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
+import { Alert, Input, Space, Spin, Tooltip } from "antd";
+import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
+import { useQuery } from "@apollo/client";
+import { phone } from "phone";
+import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
const inputArea = useRef(null);
const [selectedMedia, setSelectedMedia] = useState([]);
+ const { t } = useTranslation();
+
+ const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
+ const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, {
+ variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
+ fetchPolicy: "cache-and-network"
+ });
+
+ const isOptedOut = !!optOutData?.phone_number_opt_out?.[0];
useEffect(() => {
inputArea.current.focus();
}, [isSending, setMessage]);
- const { t } = useTranslation();
-
const handleEnter = () => {
const selectedImages = selectedMedia.filter((i) => i.isSelected);
if ((message === "" || !message) && selectedImages.length === 0) return;
+ if (isOptedOut) return; // Prevent sending if phone number is opted out
logImEXEvent("messaging_send_message");
if (selectedImages.length < 11) {
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
messagingServiceSid: bodyshop.messagingservicesid,
conversationid: conversation.id,
selectedMedia: selectedImages,
- imexshopid: bodyshop.imexshopid
+ imexshopid: bodyshop.imexshopid,
+ bodyshopid: bodyshop.id
};
sendMessage(newMessage);
setSelectedMedia(
@@ -56,47 +68,67 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
};
return (
-
-
-
-
- setMessage(e.target.value)}
- onPressEnter={(event) => {
- event.preventDefault();
- if (!!!event.shiftKey) handleEnter();
- }}
- />
-
-
-
+ {isOptedOut && (
+
+ }
+ message={t("messaging.errors.no_consent")}
+ type="error"
/>
- }
- />
-
+
+ )}
+
+ {!isOptedOut && (
+ <>
+
+
+ >
+ )}
+
+ setMessage(e.target.value)}
+ onPressEnter={(event) => {
+ event.preventDefault();
+ if (!event.shiftKey && !isOptedOut) handleEnter();
+ }}
+ />
+
+ {!isOptedOut && (
+
+ )}
+
+
+ }
+ />
+
+
);
}
diff --git a/client/src/components/feature-wrapper/feature-wrapper.component.jsx b/client/src/components/feature-wrapper/feature-wrapper.component.jsx
index 7dabea2ac..507a05923 100644
--- a/client/src/components/feature-wrapper/feature-wrapper.component.jsx
+++ b/client/src/components/feature-wrapper/feature-wrapper.component.jsx
@@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
}
return (
bodyshop?.features?.allAccess ||
- bodyshop?.features?.[featureName] ||
- dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
+ (typeof bodyshop?.features?.[featureName] === "boolean"
+ ? bodyshop?.features?.[featureName]
+ : dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs()))
);
}
diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx
index a13c258e1..826edea54 100644
--- a/client/src/components/header/header.component.jsx
+++ b/client/src/components/header/header.component.jsx
@@ -15,6 +15,7 @@ import {
HomeFilled,
ImportOutlined,
LineChartOutlined,
+ OneToOneOutlined,
PaperClipOutlined,
PhoneOutlined,
PlusCircleOutlined,
@@ -24,6 +25,7 @@ import {
TeamOutlined,
ToolFilled,
UnorderedListOutlined,
+ UsergroupAddOutlined,
UserOutlined
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
@@ -40,6 +42,7 @@ import { RiSurveyLine } from "react-icons/ri";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
+import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
@@ -47,11 +50,10 @@ import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import day from "../../utils/day.js";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
+import { useIsEmployee } from "../../utils/useIsEmployee.js";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
-import { useSocket } from "../../contexts/SocketIO/useSocket.js";
-import { useIsEmployee } from "../../utils/useIsEmployee.js";
// Redux mappings
const mapStateToProps = createStructuredSelector({
@@ -642,17 +644,32 @@ function Header({
label: t("menus.header.help"),
onClick: () => window.open("https://help.imex.online/", "_blank")
},
- ...(InstanceRenderManager({ imex: true, rome: false })
- ? [
- {
- key: "rescue",
- id: "header-rescue",
- icon:
,
- label: t("menus.header.rescueme"),
- onClick: () => window.open("https://imexrescue.com/", "_blank")
- }
- ]
- : []),
+ {
+ key: "remoteassist",
+ id: "header-remote-assist",
+ icon:
,
+ label: t("menus.header.remoteassist"),
+ children: [
+ ...(InstanceRenderManager({ imex: true, rome: false })
+ ? [
+ {
+ key: "rescue",
+ id: "header-rescue",
+ icon:
,
+ label: t("menus.header.rescueme"),
+ onClick: () => window.open("https://imexrescue.com/", "_blank")
+ }
+ ]
+ : []),
+ {
+ key: "rescue-zoho",
+ id: "header-rescue-zoho",
+ icon:
,
+ label: t("menus.header.rescuemezoho"),
+ onClick: () => window.open("https://join.zoho.com/", "_blank")
+ }
+ ]
+ },
{
key: "shiftclock",
id: "header-shiftclock",
diff --git a/client/src/components/job-at-change/schedule-event.component.jsx b/client/src/components/job-at-change/schedule-event.component.jsx
index 6f7780ba8..6438308da 100644
--- a/client/src/components/job-at-change/schedule-event.component.jsx
+++ b/client/src/components/job-at-change/schedule-event.component.jsx
@@ -395,32 +395,33 @@ export function ScheduleEventComponent({
) : (
)}
- {event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
-
-
-
- ) : (
-
{
- if (event.job?.id) {
- e.stopPropagation();
- getJobDetails();
- }
- }}
- getPopupContainer={(trigger) => trigger.parentNode}
- trigger="click"
- >
-
-
- )}
+ {event.job &&
+ (HasFeatureAccess({ featureName: "checklist", bodyshop }) ? (
+
+
+
+ ) : (
+
{
+ if (event.job?.id) {
+ e.stopPropagation();
+ getJobDetails();
+ }
+ }}
+ getPopupContainer={(trigger) => trigger.parentNode}
+ trigger="click"
+ >
+
+
+ ))}
);
diff --git a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx
index 18f5d025e..29ced4f09 100644
--- a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx
+++ b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx
@@ -1,7 +1,7 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, Switch } from "antd";
import queryString from "query-string";
-import React, { useState } from "react";
+import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate, useParams } from "react-router-dom";
@@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../../../firebase/firebase.utils";
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
-import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
@@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
const [loading, setLoading] = useState(false);
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
- const [updateOwner] = useMutation(UPDATE_OWNER);
const notification = useNotification();
const { jobId } = useParams();
@@ -129,24 +127,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
}
}
- if (type === "intake" && job.owner && job.owner.id) {
- //Updae Owner Allow to Text
- const updateOwnerResult = await updateOwner({
- variables: {
- ownerId: job.owner.id,
- owner: { allow_text_message: values.allow_text_message }
- }
- });
-
- if (!!updateOwnerResult.errors) {
- notification["error"]({
- message: t("checklist.errors.complete", {
- error: JSON.stringify(result.errors)
- })
- });
- }
- }
-
setLoading(false);
if (!!!result.errors) {
@@ -189,7 +169,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
initialValues={{
...(type === "intake" && {
addToProduction: true,
- allow_text_message: job.owner && job.owner.allow_text_message,
scheduled_completion:
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
(job &&
@@ -228,14 +207,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
>