diff --git a/src/client/index.ts b/src/client/index.ts index a1df743..2c07e16 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -31,6 +31,7 @@ export type { BaseCardTagSchema } from './models/BaseCardTagSchema'; export type { BaseEnumListSchema } from './models/BaseEnumListSchema'; export type { BaseEnumSchema } from './models/BaseEnumSchema'; export type { BaseMarketplaceSchema } from './models/BaseMarketplaceSchema'; +export type { BaseMessageSchema } from './models/BaseMessageSchema'; export type { BaseProjectSchema } from './models/BaseProjectSchema'; export type { BaseShippingWarehouseSchema } from './models/BaseShippingWarehouseSchema'; export type { BaseStatusSchema } from './models/BaseStatusSchema'; @@ -114,6 +115,7 @@ export type { CardUpdateServiceQuantityRequest } from './models/CardUpdateServic export type { CardUpdateServiceQuantityResponse } from './models/CardUpdateServiceQuantityResponse'; export type { CardUpdateServiceRequest } from './models/CardUpdateServiceRequest'; export type { CardUpdateServiceResponse } from './models/CardUpdateServiceResponse'; +export type { ChatSchema } from './models/ChatSchema'; export type { CityBreakdownFromExcelSchema } from './models/CityBreakdownFromExcelSchema'; export type { ClientCreateRequest } from './models/ClientCreateRequest'; export type { ClientCreateResponse } from './models/ClientCreateResponse'; @@ -142,6 +144,8 @@ export type { CreateCardBillResponse } from './models/CreateCardBillResponse'; export type { CreateCardGroupRequest } from './models/CreateCardGroupRequest'; export type { CreateCardsFromExcelRequest } from './models/CreateCardsFromExcelRequest'; export type { CreateCardsFromExcelResponse } from './models/CreateCardsFromExcelResponse'; +export type { CreateChatRequest } from './models/CreateChatRequest'; +export type { CreateChatResponse } from './models/CreateChatResponse'; export type { CreateDepartmentRequest } from './models/CreateDepartmentRequest'; export type { CreateDepartmentResponse } from './models/CreateDepartmentResponse'; export type { CreateDepartmentSectionRequest } from './models/CreateDepartmentSectionRequest'; @@ -185,6 +189,7 @@ export type { DeleteDepartmentResponse } from './models/DeleteDepartmentResponse export type { DeleteDepartmentSectionResponse } from './models/DeleteDepartmentSectionResponse'; export type { DeleteMarketplaceRequest } from './models/DeleteMarketplaceRequest'; export type { DeleteMarketplaceResponse } from './models/DeleteMarketplaceResponse'; +export type { DeleteMessageResponse } from './models/DeleteMessageResponse'; export type { DeletePalletResponse } from './models/DeletePalletResponse'; export type { DeletePaymentRecordRequest } from './models/DeletePaymentRecordRequest'; export type { DeletePaymentRecordResponse } from './models/DeletePaymentRecordResponse'; @@ -243,11 +248,15 @@ export type { GetBoardsResponse } from './models/GetBoardsResponse'; export type { GetCardBillById } from './models/GetCardBillById'; export type { GetCardProductsBarcodesPdfRequest } from './models/GetCardProductsBarcodesPdfRequest'; export type { GetCardProductsBarcodesPdfResponse } from './models/GetCardProductsBarcodesPdfResponse'; +export type { GetChatRequest } from './models/GetChatRequest'; +export type { GetChatResponse } from './models/GetChatResponse'; export type { GetClientMarketplacesRequest } from './models/GetClientMarketplacesRequest'; export type { GetClientMarketplacesResponse } from './models/GetClientMarketplacesResponse'; export type { GetDepartmentSectionsResponse } from './models/GetDepartmentSectionsResponse'; export type { GetDepartmentsResponse } from './models/GetDepartmentsResponse'; export type { GetManagersResponse } from './models/GetManagersResponse'; +export type { GetMessagesRequest } from './models/GetMessagesRequest'; +export type { GetMessagesResponse } from './models/GetMessagesResponse'; export type { GetPaymentRecordsResponse } from './models/GetPaymentRecordsResponse'; export type { GetPlannedWorkShiftsResponse } from './models/GetPlannedWorkShiftsResponse'; export type { GetProductBarcodePdfRequest } from './models/GetProductBarcodePdfRequest'; @@ -276,6 +285,7 @@ export type { ManageEmployeeRequest } from './models/ManageEmployeeRequest'; export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceSchema } from './models/MarketplaceSchema'; +export type { MessageSchema } from './models/MessageSchema'; export type { ModuleSchema } from './models/ModuleSchema'; export type { NotificationChannel } from './models/NotificationChannel'; export type { OptionalShippingWarehouseSchema } from './models/OptionalShippingWarehouseSchema'; @@ -328,6 +338,8 @@ export type { ResidualBoxSchema } from './models/ResidualBoxSchema'; export type { ResidualPalletSchema } from './models/ResidualPalletSchema'; export type { ResidualProductSchema } from './models/ResidualProductSchema'; export type { RoleSchema } from './models/RoleSchema'; +export type { SendMessageRequest } from './models/SendMessageRequest'; +export type { SendMessageResponse } from './models/SendMessageResponse'; export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest'; export type { ServiceCategoryReorderResponse } from './models/ServiceCategoryReorderResponse'; export type { ServiceCategorySchema } from './models/ServiceCategorySchema'; @@ -359,6 +371,8 @@ export type { SwitchTagRequest } from './models/SwitchTagRequest'; export type { SwitchTagResponse } from './models/SwitchTagResponse'; export type { SynchronizeMarketplaceRequest } from './models/SynchronizeMarketplaceRequest'; export type { TaskInfoResponse } from './models/TaskInfoResponse'; +export type { TgGroupSchema } from './models/TgGroupSchema'; +export type { TgUserSchema } from './models/TgUserSchema'; export type { TimeTrackingData } from './models/TimeTrackingData'; export type { TimeTrackingRecord } from './models/TimeTrackingRecord'; export type { TransactionSchemaBase } from './models/TransactionSchemaBase'; @@ -435,6 +449,7 @@ export { BoardService } from './services/BoardService'; export { CardService } from './services/CardService'; export { CardGroupService } from './services/CardGroupService'; export { CardTagService } from './services/CardTagService'; +export { ChatService } from './services/ChatService'; export { ClientService } from './services/ClientService'; export { DepartmentService } from './services/DepartmentService'; export { MarketplaceService } from './services/MarketplaceService'; diff --git a/src/client/models/BaseMessageSchema.ts b/src/client/models/BaseMessageSchema.ts new file mode 100644 index 0000000..ac9cd04 --- /dev/null +++ b/src/client/models/BaseMessageSchema.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type BaseMessageSchema = { + text: string; + chatId: number; +}; + diff --git a/src/client/models/CardSchema.ts b/src/client/models/CardSchema.ts index 0759de2..f66884b 100644 --- a/src/client/models/CardSchema.ts +++ b/src/client/models/CardSchema.ts @@ -12,6 +12,7 @@ import type { CardProductSchema } from './CardProductSchema'; import type { CardServiceSchema } from './CardServiceSchema'; import type { CardStatusHistorySchema } from './CardStatusHistorySchema'; import type { CardTagSchema } from './CardTagSchema'; +import type { ChatSchema } from './ChatSchema'; import type { ClientSchema } from './ClientSchema'; import type { PalletSchema } from './PalletSchema'; import type { ShippingWarehouseSchema } from './ShippingWarehouseSchema'; @@ -42,5 +43,6 @@ export type CardSchema = { employees?: Array; tags?: Array; attributes: Array; + chat: (ChatSchema | null); }; diff --git a/src/client/models/ChatSchema.ts b/src/client/models/ChatSchema.ts new file mode 100644 index 0000000..d5aef89 --- /dev/null +++ b/src/client/models/ChatSchema.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TgGroupSchema } from './TgGroupSchema'; +export type ChatSchema = { + id: number; + clientId: (number | null); + cardId: (number | null); + tgGroup: (TgGroupSchema | null); +}; + diff --git a/src/client/models/ClientDetailedSchema.ts b/src/client/models/ClientDetailedSchema.ts index a183a2a..f166c2b 100644 --- a/src/client/models/ClientDetailedSchema.ts +++ b/src/client/models/ClientDetailedSchema.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { BarcodeTemplateSchema } from './BarcodeTemplateSchema'; +import type { ChatSchema } from './ChatSchema'; import type { ClientDetailsSchema } from './ClientDetailsSchema'; import type { ResidualBoxSchema } from './ResidualBoxSchema'; import type { ResidualPalletSchema } from './ResidualPalletSchema'; @@ -13,6 +14,7 @@ export type ClientDetailedSchema = { barcodeTemplate?: (BarcodeTemplateSchema | null); comment?: (string | null); details?: (ClientDetailsSchema | null); + chat?: (ChatSchema | null); pallets?: Array; boxes?: Array; }; diff --git a/src/client/models/ClientSchema.ts b/src/client/models/ClientSchema.ts index b59f6af..3c74eb5 100644 --- a/src/client/models/ClientSchema.ts +++ b/src/client/models/ClientSchema.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { BarcodeTemplateSchema } from './BarcodeTemplateSchema'; +import type { ChatSchema } from './ChatSchema'; import type { ClientDetailsSchema } from './ClientDetailsSchema'; export type ClientSchema = { id: number; @@ -11,5 +12,6 @@ export type ClientSchema = { barcodeTemplate?: (BarcodeTemplateSchema | null); comment?: (string | null); details?: (ClientDetailsSchema | null); + chat?: (ChatSchema | null); }; diff --git a/src/client/models/CreateChatRequest.ts b/src/client/models/CreateChatRequest.ts new file mode 100644 index 0000000..2169420 --- /dev/null +++ b/src/client/models/CreateChatRequest.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CreateChatRequest = { + clientId: number; + cardId: (number | null); +}; + diff --git a/src/client/models/CreateChatResponse.ts b/src/client/models/CreateChatResponse.ts new file mode 100644 index 0000000..9fbbaed --- /dev/null +++ b/src/client/models/CreateChatResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CreateChatResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/DeleteMessageResponse.ts b/src/client/models/DeleteMessageResponse.ts new file mode 100644 index 0000000..7dfef39 --- /dev/null +++ b/src/client/models/DeleteMessageResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type DeleteMessageResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/GetChatRequest.ts b/src/client/models/GetChatRequest.ts new file mode 100644 index 0000000..7d3340f --- /dev/null +++ b/src/client/models/GetChatRequest.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type GetChatRequest = { + clientId: number; + cardId: (number | null); +}; + diff --git a/src/client/models/GetChatResponse.ts b/src/client/models/GetChatResponse.ts new file mode 100644 index 0000000..73c192d --- /dev/null +++ b/src/client/models/GetChatResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ChatSchema } from './ChatSchema'; +export type GetChatResponse = { + chat: (ChatSchema | null); +}; + diff --git a/src/client/models/GetMessagesRequest.ts b/src/client/models/GetMessagesRequest.ts new file mode 100644 index 0000000..b2efe10 --- /dev/null +++ b/src/client/models/GetMessagesRequest.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type GetMessagesRequest = { + chatId: number; + offset: number; + limit: number; +}; + diff --git a/src/client/models/GetMessagesResponse.ts b/src/client/models/GetMessagesResponse.ts new file mode 100644 index 0000000..afc29d7 --- /dev/null +++ b/src/client/models/GetMessagesResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { MessageSchema } from './MessageSchema'; +export type GetMessagesResponse = { + messages: Array; +}; + diff --git a/src/client/models/MessageSchema.ts b/src/client/models/MessageSchema.ts new file mode 100644 index 0000000..e452a3e --- /dev/null +++ b/src/client/models/MessageSchema.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { TgUserSchema } from './TgUserSchema'; +export type MessageSchema = { + text: string; + chatId: number; + id: number; + createdAt: string; + tgSender: (TgUserSchema | null); + status: string; +}; + diff --git a/src/client/models/SendMessageRequest.ts b/src/client/models/SendMessageRequest.ts new file mode 100644 index 0000000..c1db847 --- /dev/null +++ b/src/client/models/SendMessageRequest.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { BaseMessageSchema } from './BaseMessageSchema'; +export type SendMessageRequest = { + message: BaseMessageSchema; +}; + diff --git a/src/client/models/SendMessageResponse.ts b/src/client/models/SendMessageResponse.ts new file mode 100644 index 0000000..f9ebde1 --- /dev/null +++ b/src/client/models/SendMessageResponse.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type SendMessageResponse = { + ok: boolean; + message: string; +}; + diff --git a/src/client/models/TgGroupSchema.ts b/src/client/models/TgGroupSchema.ts new file mode 100644 index 0000000..c2186d4 --- /dev/null +++ b/src/client/models/TgGroupSchema.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TgGroupSchema = { + tgGroupId: number; + tgInviteLink: string; +}; + diff --git a/src/client/models/TgUserSchema.ts b/src/client/models/TgUserSchema.ts new file mode 100644 index 0000000..ac123b9 --- /dev/null +++ b/src/client/models/TgUserSchema.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type TgUserSchema = { + id: number; + firstName: string; + lastName: string; + username: string; +}; + diff --git a/src/client/services/ChatService.ts b/src/client/services/ChatService.ts new file mode 100644 index 0000000..457c374 --- /dev/null +++ b/src/client/services/ChatService.ts @@ -0,0 +1,119 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CreateChatRequest } from '../models/CreateChatRequest'; +import type { CreateChatResponse } from '../models/CreateChatResponse'; +import type { DeleteMessageResponse } from '../models/DeleteMessageResponse'; +import type { GetChatRequest } from '../models/GetChatRequest'; +import type { GetChatResponse } from '../models/GetChatResponse'; +import type { GetMessagesRequest } from '../models/GetMessagesRequest'; +import type { GetMessagesResponse } from '../models/GetMessagesResponse'; +import type { SendMessageRequest } from '../models/SendMessageRequest'; +import type { SendMessageResponse } from '../models/SendMessageResponse'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class ChatService { + /** + * Send Message + * @returns SendMessageResponse Successful Response + * @throws ApiError + */ + public static sendMessage({ + requestBody, + }: { + requestBody: SendMessageRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/chat/message', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Delete Message + * @returns DeleteMessageResponse Successful Response + * @throws ApiError + */ + public static deleteMessage({ + messageId, + }: { + messageId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/chat/message/{message_id}', + path: { + 'message_id': messageId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Chat + * @returns GetChatResponse Successful Response + * @throws ApiError + */ + public static getChat({ + requestBody, + }: { + requestBody: GetChatRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/chat/', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Create Chat + * @returns CreateChatResponse Successful Response + * @throws ApiError + */ + public static createChat({ + requestBody, + }: { + requestBody: CreateChatRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/chat/create', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Messages + * @returns GetMessagesResponse Successful Response + * @throws ApiError + */ + public static getMessages({ + requestBody, + }: { + requestBody: GetMessagesRequest, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'POST', + url: '/chat/messages', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/src/components/ActionIconCopy/ActionIconCopy.tsx b/src/components/ActionIconCopy/ActionIconCopy.tsx new file mode 100644 index 0000000..aecab78 --- /dev/null +++ b/src/components/ActionIconCopy/ActionIconCopy.tsx @@ -0,0 +1,45 @@ +import { ActionIcon, rem, Tooltip } from "@mantine/core"; +import { IconCheck, IconCopy } from "@tabler/icons-react"; +import { FC } from "react"; +import { useClipboard } from "@mantine/hooks"; + +type Props = { + value: string; + onCopiedLabel: string; +}; + +export const ActionIconCopy: FC = ({ onCopiedLabel, value }) => { + const clipboard = useClipboard(); + + return ( + + { + console.log("AKLSKLSKSM"); + return clipboard.copy(value); + }} + > + {clipboard.copied ? ( + + ) : ( + + )} + + + ); +}; +export default ActionIconCopy; diff --git a/src/components/Chat/Chat.module.css b/src/components/Chat/Chat.module.css new file mode 100644 index 0000000..97e5cdf --- /dev/null +++ b/src/components/Chat/Chat.module.css @@ -0,0 +1,20 @@ + +.message { + @mixin light { + background-color: var(--mantine-color-gray-2); + } + @mixin dark { + background-color: var(--mantine-color-dark-5); + } + border-radius: 1em; +} + +.other-message { + @mixin light { + background-color: var(--mantine-color-gray-3); + } + @mixin dark { + background-color: var(--mantine-color-dark-6); + } + border-radius: 1em; +} \ No newline at end of file diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx new file mode 100644 index 0000000..6c6ac35 --- /dev/null +++ b/src/components/Chat/Chat.tsx @@ -0,0 +1,55 @@ +import { ScrollArea, Stack } from "@mantine/core"; +import Message from "./components/Message/Message.tsx"; +import MessageInput from "./components/MessageInput/MessageInput.tsx"; +import { useChatContext } from "../../pages/ClientsPage/contexts/ChatContext.tsx"; +import { MessageSchema } from "../../client"; +import { ReactNode } from "react"; +import ChatDate from "./components/ChatDate/ChatDate.tsx"; + +const Chat = () => { + const { + messages, + scrollRef, + onScrollPositionChange, + } = useChatContext(); + + const getChatElements = (): ReactNode[] => { + const elements: ReactNode[] = []; + let prevMessage: MessageSchema | null = null; + + for (let i = messages.length - 1; i >= 0; i--) { + const currMessage = messages[i]; + + if (!prevMessage || prevMessage.createdAt.substring(5, 10) != currMessage.createdAt.substring(5, 10)) { + elements.push(( + + )); + } + elements.push(( + + )); + prevMessage = currMessage; + } + + return elements; + }; + + return ( + + + + {getChatElements()} + + + + + ); +}; + +export default Chat; diff --git a/src/components/Chat/components/ChatDate/ChatDate.tsx b/src/components/Chat/components/ChatDate/ChatDate.tsx new file mode 100644 index 0000000..0eacbe0 --- /dev/null +++ b/src/components/Chat/components/ChatDate/ChatDate.tsx @@ -0,0 +1,20 @@ +import { Box, Center, Pill } from "@mantine/core"; +import styles from "../../Chat.module.css"; + +type Props = { + date: Date; +} + +const ChatDate = ({ date }: Props) => { + return ( +
+ + + {date.toLocaleDateString("ru-RU", { day: "numeric", month: "long" })} + + +
+ ); +}; + +export default ChatDate; diff --git a/src/components/Chat/components/Message/Message.tsx b/src/components/Chat/components/Message/Message.tsx new file mode 100644 index 0000000..52eef53 --- /dev/null +++ b/src/components/Chat/components/Message/Message.tsx @@ -0,0 +1,78 @@ +import { MessageSchema } from "../../../../client"; +import { Box, Center, em, Flex, Group, rem } from "@mantine/core"; +import { formatDateTime } from "../../../../types/utils.ts"; +import { IconAlertCircle, IconCheck, IconClock, IconTrash } from "@tabler/icons-react"; +import { useContextMenu } from "mantine-contextmenu"; +import useMessage from "./hooks/useMessage.tsx"; +import styles from "../../Chat.module.css"; +import classNames from "classnames"; + +type Props = { + message: MessageSchema; +} + +enum MessageStatuses { + SUCCESS = "SUCCESS", + ERROR = "ERROR", + SENDING = "SENDING", +} + +const Message = ({ message }: Props) => { + const isMine = !message.tgSender; + const isSuccess = message.status == MessageStatuses.SUCCESS; + const { onDeleteMessageClick } = useMessage(); + const { showContextMenu } = useContextMenu(); + + const contextMenu = () => showContextMenu([ + { + key: "delete", + onClick: () => onDeleteMessageClick(message), + title: "Удалить", + icon: , + }, + ]); + + const getStatusIcon = () => { + const size = em(18); + if (message.status == MessageStatuses.ERROR) { + return ; + } + if (message.status == MessageStatuses.SENDING) { + return ; + } + return ; + }; + + return ( + + + {!isMine && ( +
{message.tgSender!.lastName} {message.tgSender!.firstName}
+ )} +
{message.text}
+ +
+ {formatDateTime(message.createdAt).substring(11, 16)} +
+ {isMine && ( +
+ {getStatusIcon()} +
+ )} +
+
+
+ ); +}; + +export default Message; diff --git a/src/components/Chat/components/Message/hooks/useMessage.tsx b/src/components/Chat/components/Message/hooks/useMessage.tsx new file mode 100644 index 0000000..db0e6a8 --- /dev/null +++ b/src/components/Chat/components/Message/hooks/useMessage.tsx @@ -0,0 +1,23 @@ +import { ChatService, MessageSchema } from "../../../../../client"; +import { notifications } from "../../../../../shared/lib/notifications.ts"; + +const useMessage = () => { + + const onDeleteMessageClick = (message: MessageSchema) => { + ChatService.deleteMessage({ + messageId: message.id, + }) + .then(({ ok, message }) => { + if (!ok) { + notifications.error({ message }); + } + }) + .catch(err => console.log(err)); + }; + + return { + onDeleteMessageClick, + }; +}; + +export default useMessage; diff --git a/src/components/Chat/components/MessageInput/MessageInput.tsx b/src/components/Chat/components/MessageInput/MessageInput.tsx new file mode 100644 index 0000000..3e5538e --- /dev/null +++ b/src/components/Chat/components/MessageInput/MessageInput.tsx @@ -0,0 +1,45 @@ +import { Button, Group, TextInput, Tooltip } from "@mantine/core"; +import { IconSend2 } from "@tabler/icons-react"; +import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx"; +import { useForm } from "@mantine/form"; +import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx"; + +export type MessageForm = { + message: string; +} + +const MessageInput = () => { + const { sendMessage, chat } = useChatContext(); + const form = useForm({ + initialValues: { + message: "", + }, + }); + + return ( +
sendMessage(values, form))}> + + {chat?.tgGroup?.tgInviteLink && ( + + + + )} + + + +
+ ); +}; + +export default MessageInput; diff --git a/src/modules/cardModules/cardEditorTabs/ChatTab/ChatTab.tsx b/src/modules/cardModules/cardEditorTabs/ChatTab/ChatTab.tsx new file mode 100644 index 0000000..0a7fbfe --- /dev/null +++ b/src/modules/cardModules/cardEditorTabs/ChatTab/ChatTab.tsx @@ -0,0 +1,33 @@ +import { ChatContextProvider } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx"; +import ChatWrapper from "./components/ChatWrapper.tsx"; +import { useCardPageContext } from "../../../../pages/CardsPage/contexts/CardPageContext.tsx"; +import { Stack } from "@mantine/core"; +import InlineButton from "../../../../components/InlineButton/InlineButton.tsx"; +import { IconMessagePlus } from "@tabler/icons-react"; +import useChatTab from "./hooks/useChatTab.tsx"; + +const ChatTab = () => { + const { selectedCard } = useCardPageContext(); + const { onChatCreateClick } = useChatTab(); + + if (!selectedCard?.chat) { + return ( + + + + Создать чат + + + ); + } + + return ( + + + + ); +}; + +export default ChatTab; diff --git a/src/modules/cardModules/cardEditorTabs/ChatTab/components/ChatWrapper.tsx b/src/modules/cardModules/cardEditorTabs/ChatTab/components/ChatWrapper.tsx new file mode 100644 index 0000000..0c29511 --- /dev/null +++ b/src/modules/cardModules/cardEditorTabs/ChatTab/components/ChatWrapper.tsx @@ -0,0 +1,21 @@ +import { useChatContext } from "../../../../../pages/ClientsPage/contexts/ChatContext.tsx"; +import { useEffect } from "react"; +import { useCardPageContext } from "../../../../../pages/CardsPage/contexts/CardPageContext.tsx"; +import Chat from "../../../../../components/Chat/Chat.tsx"; + +const ChatWrapper = () => { + const { setChat } = useChatContext(); + const { selectedCard } = useCardPageContext(); + + if (!selectedCard) return; + + useEffect(() => { + setChat(selectedCard.chat); + }, []); + + return ( + + ); +}; + +export default ChatWrapper; diff --git a/src/modules/cardModules/cardEditorTabs/ChatTab/hooks/useChatTab.tsx b/src/modules/cardModules/cardEditorTabs/ChatTab/hooks/useChatTab.tsx new file mode 100644 index 0000000..5373182 --- /dev/null +++ b/src/modules/cardModules/cardEditorTabs/ChatTab/hooks/useChatTab.tsx @@ -0,0 +1,49 @@ +import { notifications } from "../../../../../shared/lib/notifications.ts"; +import { ChatService } from "../../../../../client"; +import { modals } from "@mantine/modals"; +import { Text } from "@mantine/core"; +import { useCardPageContext } from "../../../../../pages/CardsPage/contexts/CardPageContext.tsx"; + + +const useChatTab = () => { + const { selectedCard, refetchCard } = useCardPageContext(); + + const createChat = () => { + if (!selectedCard?.clientId) { + notifications.error({ message: "Ошибка создания чата, не указан клиент" }); + return; + } + + ChatService.createChat({ + requestBody: { + clientId: selectedCard.clientId, + cardId: selectedCard.id, + }, + }) + .then(({ ok, message }) => { + notifications.guess(ok, { message }); + refetchCard(); + }) + .catch(err => console.log(err)); + }; + + const onChatCreateClick = () => { + modals.openConfirmModal({ + title: "Создание чата", + children: ( + + Вы уверены что хотите создать чат для сделки {selectedCard?.name}? + + ), + labels: { confirm: "Да", cancel: "Нет" }, + confirmProps: { color: "green" }, + onConfirm: createChat, + }); + }; + + return { + onChatCreateClick, + }; +}; + +export default useChatTab; diff --git a/src/modules/cardModules/cardEditorTabs/ClientTab/ClientTab.tsx b/src/modules/cardModules/cardEditorTabs/ClientTab/ClientTab.tsx index ec5d684..d0c05fc 100644 --- a/src/modules/cardModules/cardEditorTabs/ClientTab/ClientTab.tsx +++ b/src/modules/cardModules/cardEditorTabs/ClientTab/ClientTab.tsx @@ -128,16 +128,18 @@ const ClientTab = () => { value={client} onChange={setClient} withLabel - disabled={!isEqual(initialValues, form.values)} + disabled={!isEqual(initialValues, form.values) || !!card?.chat} /> - - - Сохранить - - + {!card?.chat && ( + + + Сохранить + + + )} {clientDataEditor} diff --git a/src/modules/connectModules.tsx b/src/modules/connectModules.tsx index 3a81211..fd6f5e0 100644 --- a/src/modules/connectModules.tsx +++ b/src/modules/connectModules.tsx @@ -5,6 +5,7 @@ import ProductAndServiceTab from "./cardModules/cardEditorTabs/ProductAndService import EmployeesTab from "./cardModules/cardEditorTabs/EmployeesTab/EmployeesTab.tsx"; import ShippingTab from "./cardModules/cardEditorTabs/ShippingTab/ShippingTab.tsx"; import ManagerTab from "./cardModules/cardEditorTabs/ManagersTab/ManagersTab.tsx"; +import ChatTab from "./cardModules/cardEditorTabs/ChatTab/ChatTab.tsx"; const connectModules = (modules: ModulesType) => { @@ -13,6 +14,7 @@ const connectModules = (modules: ModulesType) => { modules[ModuleNames.EMPLOYEES].tab = ; modules[ModuleNames.SHIPMENT].tab = ; modules[ModuleNames.MANAGERS].tab = ; + modules[ModuleNames.CHAT].tab = ; return modules; }; diff --git a/src/modules/modules.tsx b/src/modules/modules.tsx index 0b643b4..d3ae3a7 100644 --- a/src/modules/modules.tsx +++ b/src/modules/modules.tsx @@ -4,6 +4,7 @@ import { IconCubeSend, IconUsersGroup, IconUserCog, + IconMessage, } from "@tabler/icons-react"; import ModulesType from "./types.tsx"; import connectModules from "./connectModules.tsx"; @@ -14,6 +15,7 @@ export enum ModuleNames { SHIPMENT = "shipment", EMPLOYEES = "employees", MANAGERS = "managers", + CHAT = "chat", } const modules: ModulesType = { @@ -52,6 +54,13 @@ const modules: ModulesType = { icon: , } }, + [ModuleNames.CHAT]: { + info: { + label: "Чат", + key: "chat", + icon: , + } + }, }; export const MODULES = connectModules(modules); diff --git a/src/pages/ClientsPage/ClientsPage.tsx b/src/pages/ClientsPage/ClientsPage.tsx index c896dfc..e6e2623 100644 --- a/src/pages/ClientsPage/ClientsPage.tsx +++ b/src/pages/ClientsPage/ClientsPage.tsx @@ -7,6 +7,8 @@ import { Button } from "@mantine/core"; import { modals } from "@mantine/modals"; import { ClientSchema, ClientService } from "../../client"; import { notifications } from "../../shared/lib/notifications.ts"; +import { ChatContextProvider } from "./contexts/ChatContext.tsx"; +import ClientChatDrawer from "./drawers/ClientChatDrawer.tsx"; const ClientsPage: FC = () => { const { clients, refetch } = useClientsList(); @@ -62,11 +64,15 @@ const ClientsPage: FC = () => { - + + + + ); diff --git a/src/pages/ClientsPage/components/ClientChatButton/ClientChatButton.tsx b/src/pages/ClientsPage/components/ClientChatButton/ClientChatButton.tsx new file mode 100644 index 0000000..ef6cd1e --- /dev/null +++ b/src/pages/ClientsPage/components/ClientChatButton/ClientChatButton.tsx @@ -0,0 +1,68 @@ +import { ChatService, ClientSchema } from "../../../../client"; +import { ActionIcon, Text, Tooltip } from "@mantine/core"; +import { IconMessage, IconMessagePlus } from "@tabler/icons-react"; +import { useChatContext } from "../../contexts/ChatContext.tsx"; +import { modals } from "@mantine/modals"; +import { notifications } from "../../../../shared/lib/notifications.ts"; + +type Props = { + client: ClientSchema; + refetch: () => void; +} + +const ClientChatButton = ({ client, refetch }: Props) => { + const { setChat } = useChatContext(); + + const createChat = () => { + console.log("Creating chat") + ChatService.createChat({ + requestBody: { + clientId: client.id, + cardId: null, + }, + }) + .then(({ ok, message }) => { + notifications.guess(ok, { message }); + refetch(); + }) + .catch(err => console.log(err)); + }; + + const onCreateChatClick = () => { + modals.openConfirmModal({ + title: "Создание чата", + children: ( + + Вы уверены что хотите создать чат с {client.name}? + + ), + labels: { confirm: "Да", cancel: "Нет" }, + confirmProps: { color: "green" }, + onConfirm: createChat, + }); + }; + + if (client.chat) { + return ( + + setChat(client.chat!)} + variant={"default"}> + + + + ); + } + + return ( + + + + + + ); +}; + +export default ClientChatButton; diff --git a/src/pages/ClientsPage/components/ClientsTable/ClientsTable.tsx b/src/pages/ClientsPage/components/ClientsTable/ClientsTable.tsx index c24cc16..f38db3f 100644 --- a/src/pages/ClientsPage/components/ClientsTable/ClientsTable.tsx +++ b/src/pages/ClientsPage/components/ClientsTable/ClientsTable.tsx @@ -7,13 +7,20 @@ import { ActionIcon, Flex, Tooltip } from "@mantine/core"; import { IconEdit, IconTrash } from "@tabler/icons-react"; import { CRUDTableProps } from "../../../../types/CRUDTable.tsx"; import { modals } from "@mantine/modals"; +import ClientChatButton from "../ClientChatButton/ClientChatButton.tsx"; -const ClientsTable: FC> = ({ - items, - onDelete, - onChange, -}) => { +type RefetchProps = { + refetch: () => void; +} + +const ClientsTable: FC & RefetchProps> = ({ + items, + onDelete, + onChange, + refetch, + }) => { const columns = useClientsTableColumns(); + const onEditClick = (client: ClientSchema) => { if (!onChange) return; modals.openContextModal({ @@ -26,6 +33,7 @@ const ClientsTable: FC> = ({ }, }); }; + return ( <> > = ({ enableRowActions: true, renderRowActions: ({ row }) => ( + @@ -50,8 +59,7 @@ const ClientsTable: FC> = ({ { - if (onDelete) - onDelete(row.original); + if (onDelete) onDelete(row.original); }} variant={"default"}> diff --git a/src/pages/ClientsPage/contexts/ChatContext.tsx b/src/pages/ClientsPage/contexts/ChatContext.tsx new file mode 100644 index 0000000..23a3a46 --- /dev/null +++ b/src/pages/ClientsPage/contexts/ChatContext.tsx @@ -0,0 +1,161 @@ +import React, { createContext, FC, MutableRefObject, useContext, useEffect, useRef, useState } from "react"; +import { ChatSchema, ChatService, MessageSchema } from "../../../client"; +import { notifications } from "../../../shared/lib/notifications.ts"; +import { MessageForm } from "../../../components/Chat/components/MessageInput/MessageInput.tsx"; +import { UseFormReturnType } from "@mantine/form"; +import { useDebouncedState } from "@mantine/hooks"; + +type ChatContextState = { + chat: ChatSchema | null; + setChat: (chat: ChatSchema | null) => void; + messages: MessageSchema[]; + onScrollPositionChange: (values: { x: number, y: number }) => void; + scrollRef: MutableRefObject; + sendMessage: (values: MessageForm, form: UseFormReturnType) => void; +}; + +const ChatContext = createContext(undefined); + +const useChatContextState = () => { + const [chat, setChat] = useState(null); + const [messages, setMessages] = useState([]); + const [offset, setOffset] = useState(0); + const limit = 20; + const [hasMore, setHasMore] = useState(true); + const scrollRef = useRef(null); + const [isScrollToBottom, setIsScrollToBottom] = useState(true); + const [scrollPosition, setScrollPosition] = useDebouncedState<{ x: number, y: number }>({ x: 0, y: 0 }, 400); + + const setChatValue = (chat: ChatSchema | null) => { + if (chat) { + setOffset(0); + setHasMore(true); + setScrollPosition({ x: 0, y: 0 }); + } + setChat(chat); + }; + + console.log(`has more = ${hasMore}`); + + const fetchMessages = () => { + if (!chat) return; + + ChatService.getMessages({ + requestBody: { + chatId: chat.id, + offset, + limit, + }, + }) + .then(({ messages: newMessages }) => { + if (newMessages.length < limit) setHasMore(false); + setMessages([...newMessages]); + setOffset((prev) => prev + limit); + }) + .catch(err => console.log(err)); + }; + + const fetchMessagesOnScroll = () => { + if (!chat) return; + + ChatService.getMessages({ + requestBody: { + chatId: chat.id, + offset, + limit, + }, + }) + .then(({ messages: newMessages }) => { + console.log(newMessages); + if (newMessages.length < limit) setHasMore(false); + setMessages((prev) => [...prev, ...newMessages]); + setOffset((prev) => prev + limit); + if (scrollRef.current) { + console.log("FETCH ON SCROLL") + const prevPosition = limit / offset * scrollRef.current.scrollHeight; + console.log(`PREV POSITION = ${prevPosition}`) + scrollRef.current.scrollTo({ top: prevPosition, behavior: "instant" }); + } + }) + .catch(err => console.log(err)); + }; + + useEffect(() => { + fetchMessages(); + + const interval = setInterval(fetchMessages, 2000); + + return () => clearInterval(interval); + }, [chat]); + + useEffect(() => { + if (scrollRef.current && isScrollToBottom && chat) { + setIsScrollToBottom(false); + console.log("Scroll to the bottom"); + console.log(`scrollHeight = ${scrollRef.current.scrollHeight}`); + scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }); + } + }, [messages, isScrollToBottom]); + + useEffect(() => { + if (!chat) return; + console.log(`handleScroll ${Math.abs(scrollPosition.y - 200)}`); + if (Math.abs(scrollPosition.y - 200) <= 200 && hasMore) { + console.log("handleScroll WORK"); + fetchMessagesOnScroll(); + } + }, [scrollPosition]); + + const sendMessage = (values: MessageForm, form: UseFormReturnType) => { + if (!chat) return; + + ChatService.sendMessage({ + requestBody: { + message: { + text: values.message, + chatId: chat.id, + }, + }, + }) + .then(({ ok, message }) => { + if (!ok) { + notifications.error({ message }); + } + form.reset(); + setIsScrollToBottom(true); + }) + .catch(err => console.log(err)); + }; + + return { + chat, + setChat: setChatValue, + messages, + onScrollPositionChange: setScrollPosition, + scrollRef, + sendMessage, + }; +}; + +type ChatContextProviderProps = { + children: React.ReactNode; +}; + +export const ChatContextProvider: FC = ({ children }) => { + const state = useChatContextState(); + return ( + + {children} + + ); +}; + +export const useChatContext = () => { + const context = useContext(ChatContext); + if (!context) { + throw new Error( + "useChatContext must be used within a ChatContextProvider", + ); + } + return context; +}; diff --git a/src/pages/ClientsPage/drawers/ClientChatDrawer.tsx b/src/pages/ClientsPage/drawers/ClientChatDrawer.tsx new file mode 100644 index 0000000..b3ac99c --- /dev/null +++ b/src/pages/ClientsPage/drawers/ClientChatDrawer.tsx @@ -0,0 +1,24 @@ +import { useChatContext } from "../contexts/ChatContext.tsx"; +import { Drawer } from "@mantine/core"; +import Chat from "../../../components/Chat/Chat.tsx"; + + +const ClientChatDrawer = () => { + const { chat, setChat } = useChatContext(); + + if (!chat) return; + + return ( + setChat(null)} + position={"right"} + size={"calc(50vw)"} + withCloseButton={false} + > + + + ); +}; + +export default ClientChatDrawer;