Compare commits

...

37 Commits

Author SHA1 Message Date
Patrick Fic
83a1952880 IO-3051 Replace inlince css with juice. 2024-12-05 15:30:37 -08:00
Allan Carr
a885bdec74 IO-3051 canvas-handler optimization
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-12-04 14:22:04 -08:00
Dave Richer
11b906103a Merged in release/2024-11-22 (pull request #1977)
Release/2024-11-22  /  2024-11-29 - into master-AIO - IO-2920, IO-2921, IO-2959, IO-3000, IO-3001, IO-3037, IO-3040
2024-11-30 05:02:46 +00:00
Patrick Fic
3f006f431e Merged in feature/IO-3001-us-est-scrubbing (pull request #1980)
IO-3001 Update job costing label for ttl_adjustment
2024-11-29 19:56:47 +00:00
Patrick Fic
6f2b5e4c55 IO-3001 Update job costing label for ttl_adjustment 2024-11-29 11:56:18 -08:00
Patrick Fic
50d7c5dace Merged in feature/IO-3001-us-est-scrubbing (pull request #1978)
IO-3001 Add in adjustments to subtotal scrubbing.
2024-11-29 19:34:01 +00:00
Patrick Fic
9ac27b6090 IO-3001 Add in adjustments to subtotal scrubbing. 2024-11-29 11:33:19 -08:00
Dave Richer
51a1b48da9 Merge remote-tracking branch 'origin/feature/IO-3000-messaging-sockets-migrationv2' into release/2024-11-22 2024-11-28 12:27:39 -08:00
Dave Richer
648a9b8f64 feature/IO-3000-messaging-sockets-migration2 -
- Small change

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-28 12:27:06 -08:00
Dave Richer
7402679091 Merged in feature/IO-3000-messaging-sockets-migrationv2 (pull request #1974)
Feature/IO-3000 messaging sockets migrationv2
2024-11-28 20:16:43 +00:00
Dave Richer
627174b7d3 feature/IO-3000-messaging-sockets-migration2 -
- Bring back subscription for fallback

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-28 12:14:35 -08:00
Patrick Fic
9fcc01aa9f IO-3000 FInal updates to firebase SW. 2024-11-28 12:11:30 -08:00
Patrick Fic
cb46ee5700 Merged in feature/IO-3000-messaging-sockets-migrationv2 (pull request #1973)
IO-3000 update firebase js version, and add back testing route.
2024-11-28 19:41:05 +00:00
Patrick Fic
43bf1fc8cf IO-3000 update firebase js version, and add back testing route. 2024-11-28 11:39:17 -08:00
Patrick Fic
73af18f287 Merged in feature/IO-3000-messaging-sockets-migrationv2 (pull request #1970)
IO-3000 Add back FCM notification subscribe
2024-11-28 19:06:17 +00:00
Patrick Fic
90f4977924 IO-3000 Add back FCM notification subscribe 2024-11-28 11:05:50 -08:00
Dave Richer
c3b184d17b Merged in feature/IO-3000-messaging-sockets-migrationv2 (pull request #1968)
Feature/IO-3000 messaging sockets migrationv2
2024-11-28 18:02:13 +00:00
Dave Richer
db5740d487 feature/IO-3000-messaging-sockets-migration2 -
- Various work

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-28 09:57:35 -08:00
Dave Richer
08c0da1bed feature/IO-3000-messaging-sockets-migration2 -
- Various work

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-28 09:10:23 -08:00
Allan Carr
4d35976241 Merged in feature/IO-3040-Report-Selector-Date-Range-Restriction (pull request #1965)
IO-3040 Report Selector Date Range Restriction for Prod

Approved-by: Patrick Fic
2024-11-28 15:56:25 +00:00
Allan Carr
5edbed3f0b Merged in feature/IO-3001-us-est-scrubbing (pull request #1966)
IO-3001 Correct Commenting of Button
2024-11-28 15:55:59 +00:00
Allan Carr
3d79be06de IO-3001 Correct Commenting of Button
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-27 18:02:28 -08:00
Dave Richer
2937a07379 Merged in feature/IO-3000-messaging-sockets-migrationv2 (pull request #1963)
feature/IO-3000-messaging-sockets-migration2 -
2024-11-27 22:09:27 +00:00
Dave Richer
ad1761096a feature/IO-3000-messaging-sockets-migration2 -
- found small thing

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-27 14:08:52 -08:00
Patrick Fic
6a7548d11b Merged in feature/IO-2920-cash-discounting (pull request #1962)
IO-2920 Rever test URL to correct value for intellipay.
2024-11-27 21:17:56 +00:00
Patrick Fic
affbb3f168 IO-2920 Rever test URL to correct value for intellipay. 2024-11-27 13:15:03 -08:00
Dave Richer
0522747b49 Merged in feature/IO-3000-messaging-sockets-migrationv2 (pull request #1960)
Feature/IO-3000 messaging sockets migrationv2
2024-11-27 19:36:29 +00:00
Dave Richer
aec7b40ae2 feature/IO-3000-messaging-sockets-migration2 -
-misc

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-27 11:35:28 -08:00
Dave Richer
54d319f1e8 Merge remote-tracking branch 'origin/release/2024-11-22' into feature/IO-3000-messaging-sockets-migrationv2 2024-11-27 11:30:39 -08:00
Dave Richer
8d6fba2b61 feature/IO-3000-messaging-sockets-migration2 -
-misc

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-27 11:27:34 -08:00
Dave Richer
70c31eae9e feature/IO-3000-messaging-sockets-migration2 -
- [EXISTING BUG?] The updated at timeframes do not automatically update as time passes. If you receive a message, and it is changed to a few seconds ago, if you wait a few minutes, it does not change unless you interact with it forcing a re-render. This can be solved by adding a tick state and periodically refreshing - unsure of the performance impact for many elements however.

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-27 10:21:53 -08:00
Dave Richer
5e871b024d feature/IO-3000-messaging-sockets-migration2 -
- Polling Mode indicator now pegged to socket.connected

Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-11-27 10:13:38 -08:00
Dave Richer
eb1786d634 Merged in feature/IO-2959-crisp-status-page (pull request #1958)
IO-2959 Remove debug for crisp status and add sig term handler.
2024-11-27 18:03:02 +00:00
Patrick Fic
5e8d0fddbd IO-2959 Remove debug for crisp status and add sig term handler. 2024-11-27 09:44:42 -08:00
Allan Carr
5d690fd71f Merged in feature/IO-3040-Report-Selector-Date-Range-Restriction (pull request #1956)
Feature/IO-3040 Report Selector Date Range Restriction

Approved-by: Dave Richer
2024-11-27 16:48:07 +00:00
Allan Carr
79a2d902cd Merged in feature/IO-3037-Supplement-Existing-Lines (pull request #1957)
IO-3037 Supplement Existing Lines

Approved-by: Dave Richer
2024-11-27 16:47:52 +00:00
Allan Carr
77f340d08c IO-3037 Supplement Existing Lines
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-11-27 08:40:55 -08:00
31 changed files with 957 additions and 473 deletions

View File

@@ -1,6 +1,6 @@
// Scripts for firebase and firebase messaging
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js");
// Initialize the Firebase app in the service worker by passing the generated config
let firebaseConfig;
@@ -14,7 +14,7 @@ switch (this.location.hostname) {
storageBucket: "imex-dev.appspot.com",
messagingSenderId: "759548147434",
appId: "1:759548147434:web:e8239868a48ceb36700993",
measurementId: "G-K5XRBVVB4S",
measurementId: "G-K5XRBVVB4S"
};
break;
case "test.imex.online":
@@ -24,7 +24,7 @@ switch (this.location.hostname) {
projectId: "imex-test",
storageBucket: "imex-test.appspot.com",
messagingSenderId: "991923618608",
appId: "1:991923618608:web:633437569cdad78299bef5",
appId: "1:991923618608:web:633437569cdad78299bef5"
// measurementId: "${config.measurementId}",
};
break;
@@ -38,7 +38,7 @@ switch (this.location.hostname) {
storageBucket: "imex-prod.appspot.com",
messagingSenderId: "253497221485",
appId: "1:253497221485:web:3c81c483b94db84b227a64",
measurementId: "G-NTWBKG2L0M",
measurementId: "G-NTWBKG2L0M"
};
}
@@ -49,8 +49,6 @@ const messaging = firebase.messaging();
messaging.onBackgroundMessage(function (payload) {
// Customize notification here
const channel = new BroadcastChannel("imex-sw-messages");
channel.postMessage(payload);
//self.registration.showNotification(notificationTitle, notificationOptions);
console.log("[firebase-messaging-sw.js] Received background message ", payload);
self.registration.showNotification(notificationTitle, notificationOptions);
});

View File

@@ -1,9 +1,12 @@
import { useApolloClient } from "@apollo/client";
import { getToken } from "@firebase/messaging";
import axios from "axios";
import React, { useContext, useEffect } from "react";
import { useTranslation } from "react-i18next";
import SocketContext from "../../contexts/SocketIO/socketContext";
import { messaging, requestForToken } from "../../firebase/firebase.utils";
import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss";
import SocketContext from "../../contexts/SocketIO/socketContext";
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
export function ChatAffixContainer({ bodyshop, chatVisible }) {
@@ -14,6 +17,23 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
useEffect(() => {
if (!bodyshop || !bodyshop.messagingservicesid) return;
async function SubscribeToTopicForFCMNotification() {
try {
await requestForToken();
await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, {
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
}),
type: "messaging",
imexshopid: bodyshop.imexshopid
});
} catch (error) {
console.log("Error attempting to subscribe to messaging topic: ", error);
}
}
SubscribeToTopicForFCMNotification();
//Register WS handlers
if (socket && socket.connected) {
registerMessagingHandlers({ socket, client });

View File

@@ -2,39 +2,65 @@ import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql
import { gql } from "@apollo/client";
const logLocal = (message, ...args) => {
if (import.meta.env.PROD) {
return;
if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) {
console.log(`==================== ${message} ====================`);
console.dir({ ...args });
}
console.log(`==================== ${message} ====================`);
console.dir({ ...args });
};
// Utility function to enrich conversation data
const enrichConversation = (conversation, isOutbound) => ({
...conversation,
updated_at: conversation.updated_at || new Date().toISOString(),
unreadcnt: conversation.unreadcnt || 0,
archived: conversation.archived || false,
label: conversation.label || null,
job_conversations: conversation.job_conversations || [],
messages_aggregate: conversation.messages_aggregate || {
__typename: "messages_aggregate",
aggregate: {
__typename: "messages_aggregate_fields",
count: isOutbound ? 0 : 1
}
},
__typename: "conversations"
});
export const registerMessagingHandlers = ({ socket, client }) => {
if (!(socket && client)) return;
const handleNewMessageSummary = async (message) => {
const { conversationId, newConversation, existingConversation, isoutbound } = message;
logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation });
const queryVariables = { offset: 0 };
// Utility function to enrich conversation data
const enrichConversation = (conversation, isOutbound) => ({
...conversation,
updated_at: conversation.updated_at || new Date().toISOString(),
unreadcnt: conversation.unreadcnt || 0,
archived: conversation.archived || false,
label: conversation.label || null,
job_conversations: conversation.job_conversations || [],
messages_aggregate: conversation.messages_aggregate || {
__typename: "messages_aggregate",
aggregate: {
__typename: "messages_aggregate_fields",
count: isOutbound ? 0 : 1
if (!existingConversation && conversationId) {
// Attempt to read from the cache to determine if this is actually a new conversation
try {
const cachedConversation = client.cache.readFragment({
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
fragment: gql`
fragment ExistingConversationCheck on conversations {
id
}
`
});
if (cachedConversation) {
logLocal("handleNewMessageSummary - Existing Conversation inferred from cache", {
conversationId
});
return handleNewMessageSummary({
...message,
existingConversation: true
});
}
},
__typename: "conversations"
});
} catch (error) {
logLocal("handleNewMessageSummary - Cache miss", { conversationId });
}
}
// Handle new conversation
if (!existingConversation && newConversation?.phone_num) {
@@ -49,7 +75,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
const existingConversations = queryResults?.conversations || [];
const enrichedConversation = enrichConversation(newConversation, isoutbound);
// Avoid adding duplicate conversations
if (!existingConversations.some((conv) => conv.id === enrichedConversation.id)) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -68,81 +93,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
// Handle existing conversation
if (existingConversation) {
let conversationDetails;
// Attempt to read existing conversation details from cache
try {
conversationDetails = client.cache.readFragment({
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
fragment: gql`
fragment ExistingConversation on conversations {
id
phone_num
updated_at
archived
label
unreadcnt
job_conversations {
jobid
conversationid
}
messages_aggregate {
aggregate {
count
}
}
__typename
}
`
});
} catch (error) {
logLocal("handleNewMessageSummary - Cache miss for conversation, fetching from server", { conversationId });
}
// Fetch conversation details from server if not in cache
if (!conversationDetails) {
try {
const { data } = await client.query({
query: GET_CONVERSATION_DETAILS,
variables: { conversationId },
fetchPolicy: "network-only"
});
conversationDetails = data?.conversations_by_pk;
} catch (error) {
console.error("Failed to fetch conversation details from server:", error);
return;
}
}
// Validate that conversation details were retrieved
if (!conversationDetails) {
console.error("Unable to retrieve conversation details. Skipping cache update.");
return;
}
try {
// Check if the conversation is already in the cache
const queryResults = client.cache.readQuery({
query: CONVERSATION_LIST_QUERY,
variables: queryVariables
});
const isAlreadyInCache = queryResults?.conversations.some((conv) => conv.id === conversationId);
if (!isAlreadyInCache) {
const enrichedConversation = enrichConversation(conversationDetails, isoutbound);
client.cache.modify({
id: "ROOT_QUERY",
fields: {
conversations(existingConversations = []) {
return [enrichedConversation, ...existingConversations];
}
}
});
}
// Update fields for the existing conversation in the cache
client.cache.modify({
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
fields: {
@@ -166,7 +117,11 @@ export const registerMessagingHandlers = ({ socket, client }) => {
} catch (error) {
console.error("Error updating cache for existing conversation:", error);
}
return;
}
logLocal("New Conversation Summary finished without work", { message });
};
const handleNewMessageDetailed = (message) => {
@@ -391,8 +346,13 @@ export const registerMessagingHandlers = ({ socket, client }) => {
client.cache.modify({
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
fields: {
job_conversations: (existing = [], { readField }) =>
existing.filter((jobRef) => readField("jobid", jobRef) !== fields.jobId)
job_conversations: (existing = [], { readField }) => {
return existing.filter((jobRef) => {
// Read the `jobid` field safely, even if the structure is normalized
const jobId = readField("jobid", jobRef);
return jobId !== fields.jobId;
});
}
}
});
break;
@@ -421,7 +381,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
export const unregisterMessagingHandlers = ({ socket }) => {
if (!socket) return;
socket.off("new-message");
socket.off("new-message-summary");
socket.off("new-message-detailed");
socket.off("message-changed");

View File

@@ -40,7 +40,7 @@ export function ChatArchiveButton({ conversation, bodyshop }) {
};
return (
<Button onClick={handleToggleArchive} loading={loading} type="primary">
<Button onClick={handleToggleArchive} loading={loading} className="archive-button" type="primary">
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
</Button>
);

View File

@@ -1,5 +1,5 @@
import { Badge, Card, List, Space, Tag } from "antd";
import React from "react";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { Virtuoso } from "react-virtuoso";
import { createStructuredSelector } from "reselect";
@@ -20,8 +20,25 @@ const mapDispatchToProps = (dispatch) => ({
});
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
// That comma is there for a reason, do not remove it
const [, forceUpdate] = useState(false);
// Re-render every minute
useEffect(() => {
const interval = setInterval(() => {
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
}, 60000); // 1 minute in milliseconds
return () => clearInterval(interval); // Cleanup on unmount
}, []);
// Memoize the sorted conversation list
const sortedConversationList = React.useMemo(() => {
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
}, [conversationList]);
const renderConversation = (index) => {
const item = conversationList[index];
const item = sortedConversationList[index];
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
const cardContentLeft =
item.job_conversations.length > 0
@@ -64,13 +81,10 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
);
};
// CAN DO: Can go back into virtuoso for additional fetch
// endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom
return (
<div className="chat-list-container">
<Virtuoso
data={conversationList}
data={sortedConversationList}
itemContent={(index) => renderConversation(index)}
style={{ height: "100%", width: "100%" }}
/>

View File

@@ -20,10 +20,10 @@ export function ChatConversationTitleTags({ jobConversations, bodyshop }) {
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
const { socket } = useContext(SocketContext);
const handleRemoveTag = (jobId) => {
const handleRemoveTag = async (jobId) => {
const convId = jobConversations[0].conversationid;
if (!!convId) {
removeJobConversation({
await removeJobConversation({
variables: {
conversationId: convId,
jobId: jobId
@@ -38,17 +38,18 @@ export function ChatConversationTitleTags({ jobConversations, bodyshop }) {
}
});
}
}).then(() => {
if (socket) {
// Emit the `conversation-modified` event
socket.emit("conversation-modified", {
bodyshopId: bodyshop.id,
conversationId: convId,
type: "tag-removed",
jobId: jobId
});
}
});
if (socket) {
// Emit the `conversation-modified` event
socket.emit("conversation-modified", {
bodyshopId: bodyshop.id,
conversationId: convId,
type: "tag-removed",
jobId: jobId
});
}
logImEXEvent("messaging_remove_job_tag", {
conversationId: convId,
jobId: jobId

View File

@@ -15,7 +15,7 @@ const mapDispatchToProps = () => ({});
export function ChatConversationTitle({ conversation }) {
return (
<Space wrap>
<Space className="chat-title" wrap>
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
<ChatLabelComponent conversation={conversation} />
<ChatPrintButton conversation={conversation} />

View File

@@ -1,10 +1,10 @@
import { useApolloClient, useQuery } from "@apollo/client";
import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client";
import axios from "axios";
import React, { useCallback, useContext, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import SocketContext from "../../contexts/SocketIO/socketContext";
import { GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
import { GET_CONVERSATION_DETAILS, CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ChatConversationComponent from "./chat-conversation.component";
@@ -14,11 +14,12 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
function ChatConversationContainer({ bodyshop, selectedConversation }) {
const client = useApolloClient();
const { socket } = useContext(SocketContext);
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
// Fetch conversation details
const {
loading: convoLoading,
error: convoError,
@@ -29,49 +30,80 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
nextFetchPolicy: "network-only"
});
const updateCacheWithReadMessages = useCallback(
(conversationId, messageIds) => {
if (!conversationId || !messageIds || messageIds.length === 0) return;
// Subscription for conversation updates
useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
skip: socket?.connected,
variables: { conversationId: selectedConversation },
onData: ({ data: subscriptionResult, client }) => {
// Extract the messages array from the result
const messages = subscriptionResult?.data?.messages;
if (!messages || messages.length === 0) {
console.warn("No messages found in subscription result.");
return;
}
// Mark individual messages as read
messageIds.forEach((messageId) => {
messages.forEach((message) => {
const messageRef = client.cache.identify(message);
// Write the new message to the cache
client.cache.writeFragment({
id: messageRef,
fragment: gql`
fragment NewMessage on messages {
id
status
text
isoutbound
image
image_path
userid
created_at
read
}
`,
data: message
});
// Update the conversation cache to include the new message
client.cache.modify({
id: client.cache.identify({ __typename: "messages", id: messageId }),
id: client.cache.identify({ __typename: "conversations", id: selectedConversation }),
fields: {
read() {
return true; // Mark message as read
messages(existingMessages = []) {
const alreadyExists = existingMessages.some((msg) => msg.__ref === messageRef);
if (alreadyExists) return existingMessages;
return [...existingMessages, { __ref: messageRef }];
},
updated_at() {
return message.created_at;
}
}
});
});
}
});
// Update aggregate unread count for the conversation
client.cache.modify({
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
fields: {
messages_aggregate(existingAggregate) {
return {
...existingAggregate,
aggregate: {
...existingAggregate.aggregate,
count: 0 // No unread messages remaining
}
};
const updateCacheWithReadMessages = useCallback(
(conversationId, messageIds) => {
if (!conversationId || !messageIds?.length) return;
messageIds.forEach((messageId) => {
client.cache.modify({
id: client.cache.identify({ __typename: "messages", id: messageId }),
fields: {
read: () => true
}
}
});
});
},
[client.cache]
);
// Handle WebSocket events
// WebSocket event handlers
useEffect(() => {
if (!socket || !socket.connected) return;
if (!socket?.connected) return;
const handleConversationChange = (data) => {
if (data.type === "conversation-marked-read") {
const { conversationId, messageIds } = data;
console.log("Conversation change received:", data);
updateCacheWithReadMessages(conversationId, messageIds);
}
};
@@ -81,11 +113,11 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
return () => {
socket.off("conversation-changed", handleConversationChange);
};
}, [socket, client, updateCacheWithReadMessages]);
}, [socket, updateCacheWithReadMessages]);
// Handle joining/leaving conversation
// Join and leave conversation via WebSocket
useEffect(() => {
if (!socket || !socket.connected) return;
if (!socket?.connected || !selectedConversation || !bodyshop?.id) return;
socket.emit("join-bodyshop-conversation", {
bodyshopId: bodyshop.id,
@@ -98,17 +130,14 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
conversationId: selectedConversation
});
};
}, [selectedConversation, bodyshop, socket]);
}, [socket, bodyshop, selectedConversation]);
// Handle marking conversation as read
// Mark conversation as read
const handleMarkConversationAsRead = async () => {
if (!convoData || !selectedConversation || markingAsReadInProgress) return;
if (!convoData || markingAsReadInProgress) return;
const conversation = convoData.conversations_by_pk;
if (!conversation) {
console.warn(`No data found for conversation ID: ${selectedConversation}`);
return;
}
if (!conversation) return;
const unreadMessageIds = conversation.messages
?.filter((message) => !message.read && !message.isoutbound)
@@ -116,22 +145,16 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
if (unreadMessageIds?.length > 0) {
setMarkingAsReadInProgress(true);
try {
const payload = {
await axios.post("/sms/markConversationRead", {
conversation,
imexshopid: bodyshop?.imexshopid,
bodyshopid: bodyshop?.id
};
});
console.log("Marking conversation as read:", payload);
await axios.post("/sms/markConversationRead", payload);
// Update local cache
updateCacheWithReadMessages(selectedConversation, unreadMessageIds);
} catch (error) {
console.error("Error marking conversation as read:", error.response?.data || error.message);
console.error("Error marking conversation as read:", error.message);
} finally {
setMarkingAsReadInProgress(false);
}
@@ -141,11 +164,11 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
return (
<ChatConversationComponent
subState={[convoLoading, convoError]}
conversation={convoData ? convoData.conversations_by_pk : {}}
messages={convoData ? convoData.conversations_by_pk.messages : []}
conversation={convoData?.conversations_by_pk || {}}
messages={convoData?.conversations_by_pk?.messages || []}
handleMarkConversationAsRead={handleMarkConversationAsRead}
/>
);
}
export default connect(mapStateToProps, null)(ChatConversationContainer);
export default connect(mapStateToProps)(ChatConversationContainer);

View File

@@ -1,53 +1,85 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Virtuoso } from "react-virtuoso";
import { renderMessage } from "./renderMessage";
import "./chat-message-list.styles.scss";
const SCROLL_DELAY_MS = 50;
const INITIAL_SCROLL_DELAY_MS = 100;
export default function ChatMessageListComponent({ messages }) {
const virtuosoRef = useRef(null);
const [atBottom, setAtBottom] = useState(true);
const loadedImagesRef = useRef(0);
// Scroll to the bottom after a short delay when the component mounts
const handleScrollStateChange = (isAtBottom) => {
setAtBottom(isAtBottom);
};
const resetImageLoadState = () => {
loadedImagesRef.current = 0;
};
const preloadImages = (imagePaths, onComplete) => {
resetImageLoadState();
if (imagePaths.length === 0) {
onComplete();
return;
}
imagePaths.forEach((url) => {
const img = new Image();
img.src = url;
img.onload = img.onerror = () => {
loadedImagesRef.current += 1;
if (loadedImagesRef.current === imagePaths.length) {
onComplete();
}
};
});
};
// Ensure all images are loaded on initial render
useEffect(() => {
const timer = setTimeout(() => {
if (virtuosoRef?.current?.scrollToIndex && messages?.length) {
const imagePaths = messages
.filter((message) => message.image && message.image_path?.length > 0)
.flatMap((message) => message.image_path);
preloadImages(imagePaths, () => {
if (virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({
index: messages.length - 1,
behavior: "auto" // Instantly scroll to the bottom
align: "end",
behavior: "auto"
});
}
}, INITIAL_SCROLL_DELAY_MS);
});
}, [messages]);
// Cleanup the timeout on unmount
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // ESLint is disabled for this line because we only want this to load once (valid exception)
// Scroll to the bottom after the new messages are rendered
// Handle scrolling when new messages are added
useEffect(() => {
if (virtuosoRef?.current?.scrollToIndex && messages?.length) {
const timeout = setTimeout(() => {
if (!atBottom) return;
const latestMessage = messages[messages.length - 1];
const imagePaths = latestMessage?.image_path || [];
preloadImages(imagePaths, () => {
if (virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({
index: messages.length - 1,
align: "end", // Ensure the last message is fully visible
behavior: "smooth" // Smooth scrolling
align: "end",
behavior: "smooth"
});
}, SCROLL_DELAY_MS); // Slight delay to ensure layout recalculates
// Cleanup timeout on dependency changes
return () => clearTimeout(timeout);
}
}, [messages]); // Triggered when new messages are added
}
});
}, [messages, atBottom]);
return (
<div className="chat">
<Virtuoso
ref={virtuosoRef}
data={messages}
itemContent={(index) => renderMessage(messages, index)} // Pass `messages` to renderMessage
followOutput="smooth" // Ensure smooth scrolling when new data is appended
overscan={!!messages.reduce((acc, message) => acc + (message.image_path?.length || 0), 0) ? messages.length : 0}
itemContent={(index) => renderMessage(messages, index)}
followOutput={(isAtBottom) => handleScrollStateChange(isAtBottom)}
initialTopMostItemIndex={messages.length - 1}
style={{ height: "100%", width: "100%" }}
/>
</div>

View File

@@ -1,110 +1,131 @@
.message-icon {
color: whitesmoke;
border: #000000;
position: absolute;
margin: 0 0.1rem;
bottom: 0.1rem;
right: 0.3rem;
z-index: 5;
}
.chat {
flex: 1;
display: flex;
flex-direction: column;
margin: 0.8rem 0rem;
overflow: hidden; // Ensure the content scrolls correctly
height: 100%;
width: 100%;
}
.archive-button {
height: 20px;
border-radius: 4px;
}
.chat-title {
margin-bottom: 5px;
}
.messages {
display: flex;
flex-direction: column;
padding: 0.5rem; // Add padding to avoid edge clipping
padding: 0.5rem; // Prevent edge clipping
}
.message {
position: relative;
border-radius: 20px;
padding: 0.25rem 0.8rem;
word-wrap: break-word;
.message-img {
&-img {
max-width: 10rem;
max-height: 10rem;
object-fit: contain;
margin: 0.2rem;
border-radius: 4px;
}
&-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.yours {
align-items: flex-start;
.chat-send-message-button{
margin: 0.3rem;
padding-left: 0.5rem;
}
.message-icon {
position: absolute;
bottom: 0.1rem;
right: 0.3rem;
margin: 0 0.1rem;
color: whitesmoke;
z-index: 5;
}
.msgmargin {
margin-top: 0.1rem;
margin-bottom: 0.1rem;
margin: 0.1rem 0;
}
.yours .message {
margin-right: 20%;
background-color: #eee;
position: relative;
.yours,
.mine {
display: flex;
flex-direction: column;
.message {
position: relative;
&:last-child:before,
&:last-child:after {
content: "";
position: absolute;
bottom: 0;
height: 20px;
width: 20px;
z-index: 0;
}
&:last-child:after {
width: 10px;
background: white;
z-index: 1;
}
}
}
.yours .message:last-child:before {
content: "";
position: absolute;
z-index: 0;
bottom: 0;
left: -7px;
height: 20px;
width: 20px;
background: #eee;
border-bottom-right-radius: 15px;
}
.yours .message:last-child:after {
content: "";
position: absolute;
z-index: 1;
bottom: 0;
left: -10px;
width: 10px;
height: 20px;
background: white;
border-bottom-right-radius: 10px;
/* "Yours" (incoming) message styles */
.yours {
align-items: flex-start;
.message {
margin-right: 20%;
background-color: #eee;
&:last-child:before {
left: -7px;
background: #eee;
border-bottom-right-radius: 15px;
}
&:last-child:after {
left: -10px;
border-bottom-right-radius: 10px;
}
}
}
/* "Mine" (outgoing) message styles */
.mine {
align-items: flex-end;
.message {
color: white;
margin-left: 25%;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
padding-bottom: 0.6rem;
&:last-child:before {
right: -8px;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
border-bottom-left-radius: 15px;
}
&:last-child:after {
right: -10px;
border-bottom-left-radius: 10px;
}
}
}
.mine .message {
color: white;
margin-left: 25%;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
position: relative;
padding-bottom: 0.6rem;
}
.mine .message:last-child:before {
content: "";
position: absolute;
z-index: 0;
bottom: 0;
right: -8px;
height: 20px;
width: 20px;
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
border-bottom-left-radius: 15px;
}
.mine .message:last-child:after {
content: "";
position: absolute;
z-index: 1;
bottom: 0;
right: -10px;
width: 10px;
height: 20px;
background: white;
border-bottom-left-radius: 10px;
.virtuoso-container {
flex: 1;
overflow: auto;
}

View File

@@ -7,28 +7,38 @@ import { DateTimeFormatter } from "../../utils/DateFormatter";
export const renderMessage = (messages, index) => {
const message = messages[index];
return (
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
<div className="message msgmargin">
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
<div>
{message.image_path &&
message.image_path.map((i, idx) => (
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
<a href={i} target="__blank" rel="noopener noreferrer">
<img alt="Received" className="message-img" src={i} />
</a>
</div>
))}
<div>{message.text}</div>
{/* Render images if available */}
{message.image && message.image_path?.length > 0 && (
<div className="message-images">
{message.image_path.map((url, idx) => (
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
<a href={url} target="_blank" rel="noopener noreferrer">
<img alt="Received" className="message-img" src={url} />
</a>
</div>
))}
</div>
)}
{/* Render text if available */}
{message.text && <div>{message.text}</div>}
</div>
</Tooltip>
{/* Message status icons */}
{message.status && (message.status === "sent" || message.status === "delivered") && (
<div className="message-status">
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
</div>
)}
</div>
{/* Outbound message metadata */}
{message.isoutbound && (
<div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", {

View File

@@ -108,7 +108,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
<InfoCircleOutlined />
</Tooltip>
<SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
{pollInterval > 0 && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
{!socket?.connected && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
</Space>
<ShrinkOutlined
onClick={() => toggleChatVisible()}

View File

@@ -81,7 +81,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
/>
</span>
<SendOutlined
className="imex-flex-row__margin"
className="chat-send-message-button"
// disabled={message === "" || !message}
onClick={handleEnter}
/>

View File

@@ -28,7 +28,7 @@ export function ChatTagRoContainer({ conversation, bodyshop }) {
const executeSearch = (v) => {
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
loadRo(v);
loadRo(v).catch((e) => console.error("Error in ChatTagRoContainer executeSearch:", e));
};
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
@@ -41,33 +41,34 @@ export function ChatTagRoContainer({ conversation, bodyshop }) {
variables: { conversationId: conversation.id }
});
const handleInsertTag = (value, option) => {
const handleInsertTag = async (value, option) => {
logImEXEvent("messaging_add_job_tag");
insertTag({
await insertTag({
variables: { jobId: option.key }
}).then(() => {
if (socket) {
// Find the job details from the search data
const selectedJob = data?.search_jobs.find((job) => job.id === option.key);
if (!selectedJob) return;
const newJobConversation = {
__typename: "job_conversations",
jobid: selectedJob.id,
conversationid: conversation.id,
job: {
__typename: "jobs",
...selectedJob
}
};
socket.emit("conversation-modified", {
conversationId: conversation.id,
bodyshopId: bodyshop.id,
type: "tag-added",
job_conversations: [newJobConversation]
});
}
});
if (socket) {
// Find the job details from the search data
const selectedJob = data?.search_jobs.find((job) => job.id === option.key);
if (!selectedJob) return;
const newJobConversation = {
__typename: "job_conversations",
jobid: selectedJob.id,
conversationid: conversation.id,
job: {
__typename: "jobs",
...selectedJob
}
};
socket.emit("conversation-modified", {
conversationId: conversation.id,
bodyshopId: bodyshop.id,
type: "tag-added",
job_conversations: [newJobConversation]
});
}
setOpen(false);
};
@@ -85,9 +86,10 @@ export function ChatTagRoContainer({ conversation, bodyshop }) {
handleSearch={handleSearch}
handleInsertTag={handleInsertTag}
setOpen={setOpen}
style={{ cursor: "pointer" }}
/>
) : (
<Tag onClick={() => setOpen(true)}>
<Tag style={{ cursor: "pointer" }} onClick={() => setOpen(true)}>
<PlusOutlined />
{t("messaging.actions.link")}
</Tag>

View File

@@ -1,6 +1,6 @@
import { gql, useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Col, Row, notification } from "antd";
import { Col, Row, notification } from "antd"; //import { Button, Col, Row, notification } from "antd";
import Axios from "axios";
import _ from "lodash";
import queryString from "query-string";
@@ -408,8 +408,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
updateSchComp={updateSchComp}
setSchComp={setSchComp}
/>
{
{/* currentUser.email.includes("@rome.") || currentUser.email.includes("@imex.") ? (
{/* {
currentUser.email.includes("@rome.") || currentUser.email.includes("@imex.") ? (
<Button
onClick={async () => {
for (const record of data.available_jobs) {
@@ -425,8 +425,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
>
Add all jobs as new.
</Button>
) : null */}
}
) : null
} */}
<Row gutter={[16, 16]}>
<Col span={24}>
<JobsAvailableTableComponent

View File

@@ -65,13 +65,10 @@ export const requestForToken = () => {
});
};
export const onMessageListener = () =>
new Promise((resolve) => {
onMessage(messaging, (payload) => {
console.log("Inbound FCM Message", payload);
resolve(payload);
});
});
onMessage(messaging, (payload) => {
console.log("FCM Message received. ", payload);
// ...
});
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
const state = stateProp || store.getState();

View File

@@ -2,7 +2,7 @@ import { gql } from "@apollo/client";
export const GET_ALL_JOBLINES_BY_PK = gql`
query GET_ALL_JOBLINES_BY_PK($id: uuid!) {
joblines(where: { jobid: { _eq: $id } }, order_by: { line_no: asc }) {
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }, order_by: { line_no: asc }) {
id
line_no
unq_seq

View File

@@ -145,47 +145,34 @@ middlewares.push(
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
conversations: offsetLimitPagination()
}
},
conversations: {
fields: {
job_conversations: {
keyArgs: false, // Indicates that all job_conversations share the same key
merge(existing = [], incoming) {
// Merge existing and incoming job_conversations
const merged = [
...existing,
...incoming.filter(
(incomingItem) => !existing.some((existingItem) => existingItem.__ref === incomingItem.__ref)
)
];
return merged;
}
},
messages: {
keyArgs: false, // Ignore arguments when determining uniqueness (like `order_by`).
merge(existing = [], incoming = [], { readField }) {
const existingIds = new Set(existing.map((message) => readField("id", message)));
const merged = new Map();
// Merge incoming messages, avoiding duplicates
const merged = [...existing];
incoming.forEach((message) => {
if (!existingIds.has(readField("id", message))) {
merged.push(message);
}
// Add existing data to the map
existing.forEach((jobConversation) => {
// Use `readField` to get the unique `jobid`, fallback to `__ref`
const jobId = readField("jobid", jobConversation) || jobConversation.__ref;
if (jobId) merged.set(jobId, jobConversation);
});
return merged;
// Add or replace with incoming data
incoming.forEach((jobConversation) => {
// Use `readField` to get the unique `jobid`, fallback to `__ref`
const jobId = readField("jobid", jobConversation) || jobConversation.__ref;
if (jobId) merged.set(jobId, jobConversation);
});
// Return the merged data as an array
return Array.from(merged.values());
}
}
}
}
}
});
const client = new ApolloClient({
link: ApolloLink.from(middlewares),
cache,

320
package-lock.json generated
View File

@@ -41,6 +41,7 @@
"intuit-oauth": "^4.1.3",
"ioredis": "^5.4.1",
"json-2-csv": "^5.5.6",
"juice": "^11.0.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
@@ -55,7 +56,7 @@
"soap": "^1.1.6",
"socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^10.0.3",
"ssh2-sftp-client": "^11.0.0",
"twilio": "^4.23.0",
"uuid": "^10.0.0",
"winston": "^3.17.0",
@@ -66,6 +67,7 @@
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2",
"p-limit": "^3.1.0",
"prettier": "^3.3.3",
"source-map-explorer": "^2.5.2"
},
@@ -3358,6 +3360,15 @@
"node": ">= 6.0.0"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -3926,6 +3937,15 @@
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/component-emitter": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@@ -4603,6 +4623,31 @@
"node": ">= 0.8"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
"integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/encoding-sniffer/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -4689,6 +4734,18 @@
"node": ">=6"
}
},
"node_modules/escape-goat": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz",
"integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -6045,6 +6102,69 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/juice": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/juice/-/juice-11.0.0.tgz",
"integrity": "sha512-sGF8hPz9/Wg+YXbaNDqc1Iuoaw+J/P9lBHNQKXAGc9pPNjCd4fyPai0Zxj7MRtdjMr0lcgk5PjEIkP2b8R9F3w==",
"license": "MIT",
"dependencies": {
"cheerio": "^1.0.0",
"commander": "^12.1.0",
"mensch": "^0.3.4",
"slick": "^1.12.2",
"web-resource-inliner": "^7.0.0"
},
"bin": {
"juice": "bin/juice"
},
"engines": {
"node": ">=18.17"
}
},
"node_modules/juice/node_modules/cheerio": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz",
"integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"encoding-sniffer": "^0.2.0",
"htmlparser2": "^9.1.0",
"parse5": "^7.1.2",
"parse5-htmlparser2-tree-adapter": "^7.0.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^6.19.5",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=18.17"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/juice/node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"entities": "^4.5.0"
}
},
"node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
@@ -6297,6 +6417,12 @@
"cssom": "^0.5.0"
}
},
"node_modules/mensch": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz",
"integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==",
"license": "MIT"
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
@@ -6707,8 +6833,8 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"devOptional": true,
"license": "MIT",
"optional": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
@@ -6779,6 +6905,18 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -7737,16 +7875,17 @@
}
},
"node_modules/ssh2-sftp-client": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-10.0.3.tgz",
"integrity": "sha512-Wlhasz/OCgrlqC8IlBZhF19Uw/X/dHI8ug4sFQybPE+0sDztvgvDf7Om6o7LbRLe68E7XkFZf3qMnqAvqn1vkQ==",
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-11.0.0.tgz",
"integrity": "sha512-lOjgNYtioYquhtgyHwPryFNhllkuENjvCKkUXo18w/Q4UpEffCnEUBfiOTlwFdKIhG1rhrOGnA6DeKPSF2CP6w==",
"license": "Apache-2.0",
"dependencies": {
"concat-stream": "^2.0.0",
"promise-retry": "^2.0.1",
"ssh2": "^1.15.0"
},
"engines": {
"node": ">=16.20.2"
"node": ">=18.20.4"
},
"funding": {
"type": "individual",
@@ -8292,6 +8431,15 @@
"node": ">= 4.0.0"
}
},
"node_modules/undici": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz",
"integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@@ -8353,6 +8501,15 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/valid-data-url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz",
"integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -8377,6 +8534,131 @@
"node": ">=6.0"
}
},
"node_modules/web-resource-inliner": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz",
"integrity": "sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==",
"license": "MIT",
"dependencies": {
"ansi-colors": "^4.1.1",
"escape-goat": "^3.0.0",
"htmlparser2": "^5.0.0",
"mime": "^2.4.6",
"valid-data-url": "^3.0.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/web-resource-inliner/node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.2.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/web-resource-inliner/node_modules/domhandler": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz",
"integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.0.1"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/web-resource-inliner/node_modules/domutils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
"domhandler": "^4.2.0"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.2.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/web-resource-inliner/node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/web-resource-inliner/node_modules/htmlparser2": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz",
"integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
"domhandler": "^3.3.0",
"domutils": "^2.4.2",
"entities": "^2.0.0"
},
"funding": {
"url": "https://github.com/fb55/htmlparser2?sponsor=1"
}
},
"node_modules/web-resource-inliner/node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -8405,6 +8687,30 @@
"node": ">=0.8.0"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
@@ -8691,8 +8997,8 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"devOptional": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},

View File

@@ -51,6 +51,7 @@
"intuit-oauth": "^4.1.3",
"ioredis": "^5.4.1",
"json-2-csv": "^5.5.6",
"juice": "^11.0.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",

View File

@@ -24,6 +24,8 @@ const { ElastiCacheClient, DescribeCacheClustersCommand } = require("@aws-sdk/cl
const { InstanceRegion } = require("./server/utils/instanceMgr");
const StartStatusReporter = require("./server/utils/statusReporter");
const cleanupTasks = [];
let isShuttingDown = false;
const CLUSTER_RETRY_BASE_DELAY = 100;
const CLUSTER_RETRY_MAX_DELAY = 5000;
const CLUSTER_RETRY_JITTER = 100;
@@ -298,7 +300,14 @@ const main = async () => {
applyRoutes({ app });
redisSocketEvents({ io: ioRedis, redisHelpers, ioHelpers, logger });
StartStatusReporter();
const StatusReporter = StartStatusReporter();
registerCleanupTask(async () => {
StatusReporter.end();
});
// Add SIGTERM signal handler
process.on("SIGTERM", handleSigterm);
process.on("SIGINT", handleSigterm); // Optional: Handle Ctrl+C
try {
await server.listen(port);
@@ -317,3 +326,33 @@ main().catch((error) => {
// Note: If we want the app to crash on all uncaught async operations, we would
// need to put a `process.exit(1);` here
});
// Register a cleanup task
function registerCleanupTask(task) {
cleanupTasks.push(task);
}
// SIGTERM handler
async function handleSigterm() {
if (isShuttingDown) {
logger.log("sigterm-api", "WARN", null, null, { message: "Shutdown already in progress, ignoring signal." });
return;
}
isShuttingDown = true;
logger.log("sigterm-api", "WARN", null, null, { message: "SIGTERM Received. Starting graceful shutdown." });
try {
for (const task of cleanupTasks) {
logger.log("sigterm-api", "WARN", null, null, { message: `Running cleanup task: ${task.name}` });
await task();
}
logger.log("sigterm-api", "WARN", null, null, { message: `All cleanup tasks completed.` });
} catch (error) {
logger.log("sigterm-api-error", "ERROR", null, null, { message: error.message, stack: error.stack });
}
process.exit(0);
}

View File

@@ -253,7 +253,7 @@ const sendNotification = async (req, res) => {
admin
.messaging()
.send({
topic: "PRD_PATRICK-messaging",
topic: req.body.topic,
notification: {
title: `ImEX Online Message - `,
body: "Test Noti."

View File

@@ -14,7 +14,7 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const domain = process.env.NODE_ENV ? "secure" : "secure";
const domain = process.env.NODE_ENV ? "secure" : "test";
const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");
const { InstanceRegion } = require("../utils/instanceMgr");

View File

@@ -857,8 +857,8 @@ function GenerateCostingData(job) {
summaryData.totalSales = summaryData.totalSales.add(Adjustment);
//Add to lines.
costCenterData.push({
id: "Adj",
cost_center: "Adjustment",
id: "AdjEst",
cost_center: "Adjustment (Est. Match)",
sale_labor: Dinero().toFormat(),
sale_labor_dinero: Dinero(),
sale_parts: Dinero().toFormat(),

View File

@@ -73,7 +73,16 @@ async function TotalsServerSide(req, res) {
job.cieca_ttl.data.n_ttl_amt === job.cieca_ttl.data.g_ttl_amt //It looks like sometimes, gross and net are the same, but they shouldn't be.
? job.cieca_ttl.data.n_ttl_amt - job.cieca_ttl.data.g_tax
: job.cieca_ttl.data.g_ttl_amt - job.cieca_ttl.data.g_tax; //If they are, adjust the gross total down by the tax amount.
const ttlDifference = emsTotal - ret.totals.subtotal.getAmount() / 100;
const ttlDifference =
emsTotal -
ret.totals.subtotal
.add(
Dinero({
amount: Math.round((job.adjustment_bottom_line || 0) * 100)
}).multiply(-1) //Add back in the adjustment to the subtotal. We don't want to scrub it twice.
)
.getAmount() /
100;
if (Math.abs(ttlDifference) > 0.0) {
//If difference is greater than a pennny, we need to adjust it.

View File

@@ -0,0 +1,20 @@
const { isObject } = require("lodash");
const validateCanvasRequestMiddleware = (req, res, next) => {
const { w, h, values, keys, override } = req.body;
if (!values || !keys) {
return res.status(400).send("Missing required data");
}
if (override && !isObject(override)) {
return res.status(400).send("Override must be an object");
}
if (w && (!Number.isFinite(w) || w <= 0)) {
return res.status(400).send("Width must be a positive number");
}
if (h && (!Number.isFinite(h) || h <= 0)) {
return res.status(400).send("Height must be a positive number");
}
next();
};
module.exports = validateCanvasRequestMiddleware;

View File

@@ -5,89 +5,120 @@ const logger = require("../utils/logger");
const { backgroundColors, borderColors } = require("./canvas-colors");
const { isObject, defaultsDeep, isNumber } = require("lodash");
let isProcessing = false;
const requestQueue = [];
const processCanvasRequest = async (req, res) => {
try {
const { w, h, values, keys, override } = req.body;
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Set dimensions with defaults
const width = isNumber(w) ? w : 500;
const height = isNumber(h) ? h : 275;
const configuration = {
type: "doughnut",
data: {
labels: keys,
datasets: [
{
data: values,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1
}
]
},
options: {
animation: false,
devicePixelRatio: 4,
responsive: false,
maintainAspectRatio: true,
circumference: 180,
rotation: -90,
plugins: {
legend: {
labels: {
boxWidth: 20,
font: {
family: "'Montserrat'",
size: 10,
style: "normal",
weight: "normal"
}
},
position: "left"
}
}
}
};
// If we have a valid override object, merge it with the default configuration object.
// This allows for you to override the default configuration with a custom one.
const defaults = () => {
if (!override || !isObject(override)) {
return configuration;
}
return defaultsDeep(override, configuration);
};
// Generate chart
let canvas = createCanvas(width, height);
let ctx = canvas.getContext("2d");
let chart = new Chart(ctx, defaults());
const result = canvas.toDataURL();
chart.destroy();
canvas.width = 0;
canvas.height = 0;
ctx = null;
canvas = null;
chart = null;
res.status(200).send(result);
} catch (error) {
if (chart) chart.destroy();
if (canvas) {
canvas.width = 0;
canvas.height = 0;
}
ctx = null;
canvas = null;
chart = null;
logger.log("inbound-canvas-creation", "error", "jsr", null, { error: error.message, stack: error.stack });
res.status(500).send("Error generating canvas");
}
};
const processNextInQueue = async () => {
if (requestQueue.length === 0) {
isProcessing = false;
return;
}
const { req, res } = requestQueue.shift();
await processCanvasRequest(req, res);
processNextInQueue();
};
exports.canvastest = function (req, res) {
//console.log("Incoming test request.", req);
res.status(200).send("OK");
};
exports.canvas = function (req, res) {
const { w, h, values, keys, override } = req.body;
//console.log("Incoming Canvas Request:", w, h, values, keys, override);
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Gate required values
if (!values || !keys) {
res.status(400).send("Missing required data");
exports.canvas = async function (req, res) {
if (isProcessing) {
if (requestQueue.length >= 100) {
// Set a maximum queue size
return res.status(503).send("Server is busy. Please try again later.");
}
requestQueue.push({ req, res });
logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
return;
}
// Override must be an object if it exists
if (override && !isObject(override)) {
res.status(400).send("Override must be an object");
return;
}
// Set the default Width and Height
let [width, height] = [500, 275];
// Allow for custom width and height
if (isNumber(w)) {
width = w;
}
if (isNumber(h)) {
height = h;
}
const configuration = {
type: "doughnut",
data: {
labels: keys,
datasets: [
{
data: values,
backgroundColor: backgroundColors,
borderColor: borderColors,
borderWidth: 1
}
]
},
options: {
devicePixelRatio: 4,
responsive: false,
maintainAspectRatio: true,
circumference: 180,
rotation: -90,
plugins: {
legend: {
labels: {
boxWidth: 20,
font: {
family: "'Montserrat'",
size: 10,
style: "normal",
weight: "normal"
}
},
position: "left"
}
}
}
};
// If we have a valid override object, merge it with the default configuration object.
// This allows for you to override the default configuration with a custom one.
const defaults = () => {
if (!override || !isObject(override)) {
return configuration;
}
return defaultsDeep(override, configuration);
};
res.status(200).send(
(() => {
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
new Chart(ctx, defaults());
return canvas.toDataURL();
})()
);
isProcessing = true;
await processCanvasRequest(req, res);
processNextInQueue();
};

View File

@@ -3,24 +3,36 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const logger = require("../utils/logger");
const inlineCssTool = require("inline-css");
//const inlineCssTool = require("inline-css");
const juice = require("juice");
exports.inlinecss = (req, res) => {
exports.inlinecss = async (req, res) => {
//Perform request validation
logger.log("email-inline-css", "DEBUG", req.user.email, null, null);
const { html, url } = req.body;
inlineCssTool(html, { url: url })
.then((inlinedHtml) => {
res.send(inlinedHtml);
})
.catch((error) => {
logger.log("email-inline-css-error", "ERROR", req.user.email, null, {
error
});
res.send(error);
try {
const inlinedHtml = juice(html, {
applyAttributesTableElements: false,
preserveMediaQueries: false,
applyWidthAttributes: false
});
res.send(inlinedHtml);
} catch (error) {
logger.log("email-inline-css-error", "ERROR", req.user.email, null, {
error
});
res.send(error.message);
}
// inlineCssTool(html, { url: url })
// .then((inlinedHtml) => {
// res.send(inlinedHtml);
// })
// .catch((error) => {
// logger.log("email-inline-css-error", "ERROR", req.user.email, null, {
// error
// });
// });
};

View File

@@ -1,11 +1,12 @@
const express = require("express");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { subscribe, unsubscribe } = require("../firebase/firebase-handler");
const { subscribe, unsubscribe, sendNotification } = require("../firebase/firebase-handler");
const router = express.Router();
router.use(validateFirebaseIdTokenMiddleware);
router.post("/subscribe", subscribe);
router.post("/unsubscribe", unsubscribe);
router.post("/sendtestnotification", sendNotification);
module.exports = router;

View File

@@ -2,10 +2,11 @@ const express = require("express");
const router = express.Router();
const { inlinecss } = require("../render/inlinecss");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const validateCanvasRequestMiddleware = require("../middleware/validateCanvasRequestMiddleware");
const { canvas } = require("../render/canvas-handler");
// Define the route for inline CSS rendering
router.post("/inlinecss", validateFirebaseIdTokenMiddleware, inlinecss);
router.post("/canvas", validateFirebaseIdTokenMiddleware, canvas);
router.post("/canvas", [validateFirebaseIdTokenMiddleware, validateCanvasRequestMiddleware], canvas);
module.exports = router;

View File

@@ -11,7 +11,7 @@ const InstanceManager = require("../utils/instanceMgr").default;
function StartStatusReporter() {
//For ImEX Online.
InstanceManager({
return InstanceManager({
executeFunction: true,
args: [],
imex: () => {
@@ -31,14 +31,14 @@ function StartStatusReporter() {
service_id: process.env.CRISP_SERVICE_IDENTIFIER, // Service ID containing the parent Node for Replica (given by Crisp)
node_id: process.env.CRISP_NODE_IDENTIFIER, // Node ID containing Replica (given by Crisp)
replica_id: getHostNameOrIP(), // Unique Replica ID for instance (ie. your IP on the LAN)
interval: 30, // Reporting interval (in seconds; defaults to 30 seconds if not set)
interval: 30 // Reporting interval (in seconds; defaults to 30 seconds if not set)
console: {
debug: (log_message, data) => logger.log("crisp-status-update", "DEBUG", null, null, { log_message, data }),
log: (log_message, data) => logger.log("crisp-status-update", "DEBUG", null, null, { log_message, data }),
warn: (log_message, data) => logger.log("crisp-status-update", "WARN", null, null, { log_message, data }),
error: (log_message, data) => logger.log("crisp-status-update", "ERROR", null, null, { log_message, data })
} // Console instance if you need to debug issues,
// console: {
// debug: (log_message, data) => logger.log("crisp-status-update", "DEBUG", null, null, { log_message, data }),
// log: (log_message, data) => logger.log("crisp-status-update", "DEBUG", null, null, { log_message, data }),
// warn: (log_message, data) => logger.log("crisp-status-update", "WARN", null, null, { log_message, data }),
// error: (log_message, data) => logger.log("crisp-status-update", "ERROR", null, null, { log_message, data })
// } // Console instance if you need to debug issues,
});
return crispStatusReporter;