diff --git a/client/package.json b/client/package.json index 6f31ce130..24b997708 100644 --- a/client/package.json +++ b/client/package.json @@ -51,6 +51,7 @@ "react-i18next": "^12.2.0", "react-icons": "^4.7.1", "react-image-lightbox": "^5.1.4", + "react-intersection-observer": "^9.4.3", "react-number-format": "^5.1.3", "react-redux": "^8.0.5", "react-resizable": "^3.0.4", diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index 816772291..ffc087ae2 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -1,5 +1,5 @@ import { Badge, List, Tag } from "antd"; -import React from "react"; +import React, { useEffect } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { setSelectedConversation } from "../../redux/messaging/messaging.actions"; @@ -7,6 +7,8 @@ import { selectSelectedConversation } from "../../redux/messaging/messaging.sele import { TimeAgoFormatter } from "../../utils/DateFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import { List as VirtualizedList, AutoSizer } from "react-virtualized"; + import "./chat-conversation-list.styles.scss"; const mapStateToProps = createStructuredSelector({ @@ -18,59 +20,86 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(setSelectedConversation(conversationId)), }); -export function ChatConversationListComponent({ +function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, + subscribeToMoreConversations, + loadMoreConversations, }) { + useEffect( + () => subscribeToMoreConversations(), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const rowRenderer = ({ index, key, style }) => { + const item = conversationList[index]; + + return ( + setSelectedConversation(item.id)} + className={`chat-list-item ${ + item.id === selectedConversation + ? "chat-list-selected-conversation" + : null + }`} + style={style} + > +
+ {item.label &&
{item.label}
} + {item.job_conversations.length > 0 ? ( +
+ {item.job_conversations.map((j, idx) => ( +
+ +
+ ))} +
+ ) : ( + {item.phone_num} + )} +
+
+
+ {item.job_conversations.length > 0 + ? item.job_conversations.map((j, idx) => ( + + {j.job.ro_number} + + )) + : null} +
+ {item.updated_at} +
+ +
+ ); + }; + return (
- ( - setSelectedConversation(item.id)} - className={`chat-list-item ${ - item.id === selectedConversation - ? "chat-list-selected-conversation" - : null - }`} - > -
- {item.label &&
{item.label}
} - {item.job_conversations.length > 0 ? ( -
- {item.job_conversations.map((j, idx) => ( -
- -
- ))} -
- ) : ( - {item.phone_num} - )} -
-
-
- {item.job_conversations.length > 0 - ? item.job_conversations.map((j, idx) => ( - - {j.job.ro_number} - - )) - : null} -
- {item.updated_at} -
- -
+ + {({ height, width }) => ( + { + if (scrollTop + clientHeight === scrollHeight) { + loadMoreConversations(); + } + }} + /> )} - /> +
); } + export default connect( mapStateToProps, mapDispatchToProps diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss index bfc83d947..20cf8f4ef 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss +++ b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss @@ -3,8 +3,9 @@ } .chat-list-container { flex: 1; - overflow: auto; + overflow: hidden; height: 100%; + border: 1px solid gainsboro; } .chat-list-item { @@ -21,4 +22,6 @@ .ro-number-tag { align-self: baseline; } + padding: 12px 24px; + border-bottom: 1px solid gainsboro; } diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index 9a4dc33eb..1c80d7c4c 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -4,15 +4,16 @@ import { ShrinkOutlined, SyncOutlined, } from "@ant-design/icons"; -import { useQuery } from "@apollo/client"; +import { useLazyQuery, useSubscription } from "@apollo/client"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { CONVERSATION_LIST_QUERY, - UNREAD_CONVERSATION_COUNT, + CONVERSATION_LIST_SUBSCRIPTION, + UNREAD_CONVERSATION_COUNT_SUBSCRIPTION, } from "../../graphql/conversations.queries"; import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; import { @@ -41,17 +42,17 @@ export function ChatPopupComponent({ const { t } = useTranslation(); const [pollInterval, setpollInterval] = useState(0); - const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, { - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - ...(pollInterval > 0 ? { pollInterval } : {}), - }); + const { data: unreadData } = useSubscription( + UNREAD_CONVERSATION_COUNT_SUBSCRIPTION + ); - const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, { + const [ + getConversations, + { loading, data, called, refetch, fetchMore, subscribeToMore }, + ] = useLazyQuery(CONVERSATION_LIST_QUERY, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", skip: !chatVisible, - ...(pollInterval > 0 ? { pollInterval } : {}), }); const fcmToken = sessionStorage.getItem("fcmtoken"); @@ -65,15 +66,22 @@ export function ChatPopupComponent({ }, [fcmToken]); useEffect(() => { - if (called && chatVisible) refetch(); - }, [chatVisible, called, refetch]); + if (called && chatVisible) + getConversations({ + variables: { + offset: 0, + }, + }); + }, [chatVisible, called, getConversations]); - // const unreadCount = data - // ? data.conversations.reduce( - // (acc, val) => val.messages_aggregate.aggregate.count + acc, - // 0 - // ) - // : 0; + const loadMoreConversations = useCallback(() => { + if (data) + fetchMore({ + variables: { + offset: data.conversations.length, + }, + }); + }, [data, fetchMore]); const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0; @@ -110,6 +118,44 @@ export function ChatPopupComponent({ ) : ( + subscribeToMore({ + document: CONVERSATION_LIST_SUBSCRIPTION, + variables: { offset: 0 }, + updateQuery: (prev, { subscriptionData }) => { + if ( + !subscriptionData.data || + subscriptionData.data.conversations.length === 0 + ) + return prev; + + let conversations = [...prev.conversations]; + const newConversations = + subscriptionData.data.conversations; + + for (const conversation of newConversations) { + const index = conversations.findIndex( + (prevConversation) => + prevConversation.id === conversation.id + ); + + if (index !== -1) { + conversations.splice(index, 1); + conversations.unshift(conversation); + + continue; + } + + conversations.unshift(conversation); + } + + return Object.assign({}, prev, { + conversations: conversations, + }); + }, + }) + } /> )} diff --git a/client/src/components/job-payments/job-payments.component.jsx b/client/src/components/job-payments/job-payments.component.jsx index ad56ebc61..65943905d 100644 --- a/client/src/components/job-payments/job-payments.component.jsx +++ b/client/src/components/job-payments/job-payments.component.jsx @@ -115,7 +115,7 @@ export function JobPayments({ render: (text, record) => ( + ); +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(PaymentMarkForExportButton); diff --git a/client/src/components/payment-modal/payment-modal.container.jsx b/client/src/components/payment-modal/payment-modal.container.jsx index a2e7e37fe..650a4857c 100644 --- a/client/src/components/payment-modal/payment-modal.container.jsx +++ b/client/src/components/payment-modal/payment-modal.container.jsx @@ -1,6 +1,6 @@ import { useMutation } from "@apollo/client"; -import { Button, Form, Modal, notification } from "antd"; +import { Button, Form, Modal, notification, Space } from "antd"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -19,6 +19,8 @@ import { import { GenerateDocument } from "../../utils/RenderTemplate"; import { TemplateList } from "../../utils/TemplateConstants"; import PaymentForm from "../payment-form/payment-form.component"; +import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component"; +import PaymentMarkForExportButton from "../payment-mark-export-button/payment-mark-export-button-component"; const mapStateToProps = createStructuredSelector({ paymentModal: selectPayment, @@ -176,12 +178,24 @@ function PaymentModalContainer({ } > + {!context || (context && !context.id) ? null : ( + + + + + )} +
diff --git a/client/src/components/payment-reexport-button/payment-reexport-button.component.jsx b/client/src/components/payment-reexport-button/payment-reexport-button.component.jsx new file mode 100644 index 000000000..a3adb5422 --- /dev/null +++ b/client/src/components/payment-reexport-button/payment-reexport-button.component.jsx @@ -0,0 +1,66 @@ +import React from "react"; +import { Button, notification } from "antd"; +import { useTranslation } from "react-i18next"; +import { UPDATE_PAYMENT } from "../../graphql/payments.queries"; +import { useMutation } from "@apollo/client"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { connect } from "react-redux"; + +const mapDispatchToProps = (dispatch) => ({ + setPaymentContext: (context) => + dispatch(setModalContext({ context: context, modal: "payment" })), +}); + +const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => { + const { t } = useTranslation(); + const [updatePayment, { loading }] = useMutation(UPDATE_PAYMENT); + + const handleClick = async () => { + const paymentUpdateResponse = await updatePayment({ + variables: { + paymentId: payment.id, + payment: { + exportedat: null, + }, + }, + }); + + if (!!!paymentUpdateResponse.errors) { + notification.open({ + type: "success", + key: "paymentsuccessexport", + message: t("payments.successes.markreexported"), + }); + + if (refetch) refetch(); + + setPaymentContext({ + actions: { + refetch, + }, + context: { + ...payment, + exportedat: null, + }, + }); + } else { + notification["error"]({ + message: t("payments.errors.exporting", { + error: JSON.stringify(paymentUpdateResponse.error), + }), + }); + } + }; + + return ( + + ); +}; + +export default connect(null, mapDispatchToProps)(PaymentReexportButton); diff --git a/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx b/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx index d34b56f9c..8337c89f8 100644 --- a/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx +++ b/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx @@ -156,7 +156,7 @@ export function PaymentsListPaginated({ render: (text, record) => (