Merge remote-tracking branch 'origin/chats'
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
"react-imask": "^7.6.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-to-print": "^2.15.1",
|
||||
"react-virtuoso": "^4.12.6",
|
||||
"reactflow": "^11.11.4",
|
||||
"recharts": "^2.13.3",
|
||||
"zod": "^3.23.8"
|
||||
|
||||
@@ -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';
|
||||
@@ -39,6 +40,7 @@ export type { BillPaymentStatus } from './models/BillPaymentStatus';
|
||||
export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest';
|
||||
export type { BoardSchema } from './models/BoardSchema';
|
||||
export type { Body_parse_deals_excel } from './models/Body_parse_deals_excel';
|
||||
export type { Body_send_messages_with_files } from './models/Body_send_messages_with_files';
|
||||
export type { Body_upload_passport_image } from './models/Body_upload_passport_image';
|
||||
export type { Body_upload_product_barcode_image } from './models/Body_upload_product_barcode_image';
|
||||
export type { Body_upload_product_image } from './models/Body_upload_product_image';
|
||||
@@ -114,6 +116,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 +145,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 +190,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';
|
||||
@@ -211,6 +217,9 @@ export type { DepartmentSchema } from './models/DepartmentSchema';
|
||||
export type { DepartmentSectionBaseSchema } from './models/DepartmentSectionBaseSchema';
|
||||
export type { DepartmentSectionBriefSchema } from './models/DepartmentSectionBriefSchema';
|
||||
export type { DepartmentSectionSchema } from './models/DepartmentSectionSchema';
|
||||
export type { EditMessageRequest } from './models/EditMessageRequest';
|
||||
export type { EditMessageResponse } from './models/EditMessageResponse';
|
||||
export type { EditMessageSchema } from './models/EditMessageSchema';
|
||||
export type { FinishPauseByShiftIdResponse } from './models/FinishPauseByShiftIdResponse';
|
||||
export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse';
|
||||
export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse';
|
||||
@@ -243,11 +252,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';
|
||||
@@ -270,12 +283,15 @@ export type { GetWorkShiftsPlanningDataRequest } from './models/GetWorkShiftsPla
|
||||
export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse';
|
||||
export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema';
|
||||
export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||
export type { LoadMessagesResponse } from './models/LoadMessagesResponse';
|
||||
export type { LoadReceiptRequest } from './models/LoadReceiptRequest';
|
||||
export type { LoadReceiptResponse } from './models/LoadReceiptResponse';
|
||||
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 { MessageFileSchema } from './models/MessageFileSchema';
|
||||
export type { MessageSchema } from './models/MessageSchema';
|
||||
export type { ModuleSchema } from './models/ModuleSchema';
|
||||
export type { NotificationChannel } from './models/NotificationChannel';
|
||||
export type { OptionalShippingWarehouseSchema } from './models/OptionalShippingWarehouseSchema';
|
||||
@@ -324,10 +340,15 @@ export type { ProjectGeneralInfoSchema } from './models/ProjectGeneralInfoSchema
|
||||
export type { ProjectSchema } from './models/ProjectSchema';
|
||||
export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema';
|
||||
export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema';
|
||||
export type { RepeatSendingMessageSchema } from './models/RepeatSendingMessageSchema';
|
||||
export type { RepeatSendingTextMessageRequest } from './models/RepeatSendingTextMessageRequest';
|
||||
export type { RepeatSendingTextMessageResponse } from './models/RepeatSendingTextMessageResponse';
|
||||
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 { SendTextMessageRequest } from './models/SendTextMessageRequest';
|
||||
export type { SendTextMessageResponse } from './models/SendTextMessageResponse';
|
||||
export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest';
|
||||
export type { ServiceCategoryReorderResponse } from './models/ServiceCategoryReorderResponse';
|
||||
export type { ServiceCategorySchema } from './models/ServiceCategorySchema';
|
||||
@@ -359,6 +380,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 +458,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';
|
||||
|
||||
9
src/client/models/BaseMessageSchema.ts
Normal file
9
src/client/models/BaseMessageSchema.ts
Normal file
@@ -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;
|
||||
};
|
||||
|
||||
8
src/client/models/Body_send_messages_with_files.ts
Normal file
8
src/client/models/Body_send_messages_with_files.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_send_messages_with_files = {
|
||||
files: Array<Blob>;
|
||||
};
|
||||
|
||||
@@ -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<CardEmployeesSchema>;
|
||||
tags?: Array<CardTagSchema>;
|
||||
attributes: Array<CardAttributeSchema>;
|
||||
chat: (ChatSchema | null);
|
||||
};
|
||||
|
||||
|
||||
12
src/client/models/ChatSchema.ts
Normal file
12
src/client/models/ChatSchema.ts
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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<ResidualPalletSchema>;
|
||||
boxes?: Array<ResidualBoxSchema>;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
9
src/client/models/CreateChatRequest.ts
Normal file
9
src/client/models/CreateChatRequest.ts
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
9
src/client/models/CreateChatResponse.ts
Normal file
9
src/client/models/CreateChatResponse.ts
Normal file
@@ -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;
|
||||
};
|
||||
|
||||
9
src/client/models/DeleteMessageResponse.ts
Normal file
9
src/client/models/DeleteMessageResponse.ts
Normal file
@@ -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;
|
||||
};
|
||||
|
||||
9
src/client/models/EditMessageRequest.ts
Normal file
9
src/client/models/EditMessageRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { EditMessageSchema } from './EditMessageSchema';
|
||||
export type EditMessageRequest = {
|
||||
message: EditMessageSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/EditMessageResponse.ts
Normal file
9
src/client/models/EditMessageResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type EditMessageResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
10
src/client/models/EditMessageSchema.ts
Normal file
10
src/client/models/EditMessageSchema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type EditMessageSchema = {
|
||||
text: string;
|
||||
chatId: number;
|
||||
id: number;
|
||||
};
|
||||
|
||||
9
src/client/models/GetChatRequest.ts
Normal file
9
src/client/models/GetChatRequest.ts
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
9
src/client/models/GetChatResponse.ts
Normal file
9
src/client/models/GetChatResponse.ts
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
10
src/client/models/GetMessagesRequest.ts
Normal file
10
src/client/models/GetMessagesRequest.ts
Normal file
@@ -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;
|
||||
};
|
||||
|
||||
9
src/client/models/GetMessagesResponse.ts
Normal file
9
src/client/models/GetMessagesResponse.ts
Normal file
@@ -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<MessageSchema>;
|
||||
};
|
||||
|
||||
9
src/client/models/LoadMessagesResponse.ts
Normal file
9
src/client/models/LoadMessagesResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type LoadMessagesResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
12
src/client/models/MessageFileSchema.ts
Normal file
12
src/client/models/MessageFileSchema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type MessageFileSchema = {
|
||||
id: number;
|
||||
filePath: string;
|
||||
type: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
};
|
||||
|
||||
19
src/client/models/MessageSchema.ts
Normal file
19
src/client/models/MessageSchema.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { MessageFileSchema } from './MessageFileSchema';
|
||||
import type { TgUserSchema } from './TgUserSchema';
|
||||
import type { UserSchema } from './UserSchema';
|
||||
export type MessageSchema = {
|
||||
text: string;
|
||||
chatId: number;
|
||||
id: number;
|
||||
createdAt: string;
|
||||
tgSender: (TgUserSchema | null);
|
||||
crmSender: (UserSchema | null);
|
||||
status: string;
|
||||
isEdited: boolean;
|
||||
file?: (MessageFileSchema | null);
|
||||
};
|
||||
|
||||
10
src/client/models/RepeatSendingMessageSchema.ts
Normal file
10
src/client/models/RepeatSendingMessageSchema.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type RepeatSendingMessageSchema = {
|
||||
text: string;
|
||||
chatId: number;
|
||||
id: number;
|
||||
};
|
||||
|
||||
9
src/client/models/RepeatSendingTextMessageRequest.ts
Normal file
9
src/client/models/RepeatSendingTextMessageRequest.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { RepeatSendingMessageSchema } from './RepeatSendingMessageSchema';
|
||||
export type RepeatSendingTextMessageRequest = {
|
||||
message: RepeatSendingMessageSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/RepeatSendingTextMessageResponse.ts
Normal file
9
src/client/models/RepeatSendingTextMessageResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type RepeatSendingTextMessageResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/SendTextMessageRequest.ts
Normal file
9
src/client/models/SendTextMessageRequest.ts
Normal file
@@ -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 SendTextMessageRequest = {
|
||||
message: BaseMessageSchema;
|
||||
};
|
||||
|
||||
9
src/client/models/SendTextMessageResponse.ts
Normal file
9
src/client/models/SendTextMessageResponse.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type SendTextMessageResponse = {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
9
src/client/models/TgGroupSchema.ts
Normal file
9
src/client/models/TgGroupSchema.ts
Normal file
@@ -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;
|
||||
};
|
||||
|
||||
11
src/client/models/TgUserSchema.ts
Normal file
11
src/client/models/TgUserSchema.ts
Normal file
@@ -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;
|
||||
};
|
||||
|
||||
214
src/client/services/ChatService.ts
Normal file
214
src/client/services/ChatService.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { Body_send_messages_with_files } from '../models/Body_send_messages_with_files';
|
||||
import type { CreateChatRequest } from '../models/CreateChatRequest';
|
||||
import type { CreateChatResponse } from '../models/CreateChatResponse';
|
||||
import type { DeleteMessageResponse } from '../models/DeleteMessageResponse';
|
||||
import type { EditMessageRequest } from '../models/EditMessageRequest';
|
||||
import type { EditMessageResponse } from '../models/EditMessageResponse';
|
||||
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 { LoadMessagesResponse } from '../models/LoadMessagesResponse';
|
||||
import type { RepeatSendingTextMessageRequest } from '../models/RepeatSendingTextMessageRequest';
|
||||
import type { RepeatSendingTextMessageResponse } from '../models/RepeatSendingTextMessageResponse';
|
||||
import type { SendTextMessageRequest } from '../models/SendTextMessageRequest';
|
||||
import type { SendTextMessageResponse } from '../models/SendTextMessageResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class ChatService {
|
||||
/**
|
||||
* Send Text Message
|
||||
* @returns SendTextMessageResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static sendTextMessage({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: SendTextMessageRequest,
|
||||
}): CancelablePromise<SendTextMessageResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/chat/message',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Edit Message
|
||||
* @returns EditMessageResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static editMessage({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: EditMessageRequest,
|
||||
}): CancelablePromise<EditMessageResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PATCH',
|
||||
url: '/chat/message',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Repeat Sending Text Message
|
||||
* @returns RepeatSendingTextMessageResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static repeatSendingTextMessage({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: RepeatSendingTextMessageRequest,
|
||||
}): CancelablePromise<RepeatSendingTextMessageResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/chat/message/repeat',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Send Messages With Files
|
||||
* @returns LoadMessagesResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static sendMessagesWithFiles({
|
||||
chatId,
|
||||
caption,
|
||||
formData,
|
||||
}: {
|
||||
chatId: number,
|
||||
caption: string,
|
||||
formData: Body_send_messages_with_files,
|
||||
}): CancelablePromise<LoadMessagesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/chat/message/files',
|
||||
query: {
|
||||
'chat_id': chatId,
|
||||
'caption': caption,
|
||||
},
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Delete Message
|
||||
* @returns DeleteMessageResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteMessage({
|
||||
messageId,
|
||||
}: {
|
||||
messageId: number,
|
||||
}): CancelablePromise<DeleteMessageResponse> {
|
||||
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<GetChatResponse> {
|
||||
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<CreateChatResponse> {
|
||||
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<GetMessagesResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/chat/messages',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Get Tg File
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getTgFileChatTgFileFileIdGet({
|
||||
fileId,
|
||||
}: {
|
||||
fileId: number,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/chat/tg-file/{file_id}',
|
||||
path: {
|
||||
'file_id': fileId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
41
src/components/ActionIconCopy/ActionIconCopy.tsx
Normal file
41
src/components/ActionIconCopy/ActionIconCopy.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
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<Props> = ({ onCopiedLabel, value }) => {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
label={onCopiedLabel}
|
||||
offset={5}
|
||||
radius="xl"
|
||||
transitionProps={{ duration: 100, transition: "slide-down" }}
|
||||
opened={clipboard.copied}>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
size="lg"
|
||||
onClick={() => clipboard.copy(value)}
|
||||
>
|
||||
{clipboard.copied ? (
|
||||
<IconCheck
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
) : (
|
||||
<IconCopy
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
export default ActionIconCopy;
|
||||
20
src/components/Chat/Chat.module.css
Normal file
20
src/components/Chat/Chat.module.css
Normal file
@@ -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;
|
||||
}
|
||||
86
src/components/Chat/Chat.tsx
Normal file
86
src/components/Chat/Chat.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import Message from "./components/Message/Message.tsx";
|
||||
import { useChatContext } from "../../pages/ClientsPage/contexts/ChatContext.tsx";
|
||||
import { MessageSchema } from "../../client";
|
||||
import { ReactNode, useCallback } from "react";
|
||||
import ChatDate from "./components/ChatDate/ChatDate.tsx";
|
||||
import MessageInput from "./components/MessageInput/MessageInput.tsx";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { Stack } from "@mantine/core";
|
||||
|
||||
const Chat = () => {
|
||||
const {
|
||||
messages,
|
||||
lastMessage,
|
||||
firstItemIndex,
|
||||
fetchMoreMessages,
|
||||
} = useChatContext();
|
||||
|
||||
const onFollowOutputHandler = useCallback(
|
||||
(atBottom: boolean) => {
|
||||
if (atBottom) {
|
||||
return "auto";
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[lastMessage],
|
||||
);
|
||||
|
||||
const itemContent = useCallback(
|
||||
(index: number, sessionData: MessageSchema) => {
|
||||
let dateComponent: ReactNode | null = null;
|
||||
const msgArrayIdx = index - firstItemIndex;
|
||||
if (msgArrayIdx < 0 || msgArrayIdx > messages.length - 1) return;
|
||||
const currMessage = messages[msgArrayIdx];
|
||||
let prevMessage = null;
|
||||
if (msgArrayIdx > 0) {
|
||||
prevMessage = messages[msgArrayIdx - 1];
|
||||
}
|
||||
if (!prevMessage || prevMessage.createdAt.substring(5, 10) != currMessage.createdAt.substring(5, 10)) {
|
||||
dateComponent = (
|
||||
<ChatDate
|
||||
key={currMessage.id + "date"}
|
||||
date={new Date(currMessage.createdAt)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack mb={"xs"} mr={"xs"}>
|
||||
{dateComponent}
|
||||
<Message
|
||||
key={`${sessionData.id}${index}`}
|
||||
message={sessionData}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
[messages],
|
||||
);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<Stack h={"96vh"} justify={"flex-end"}>
|
||||
<MessageInput />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack h={"96vh"}>
|
||||
<Virtuoso
|
||||
data={messages}
|
||||
followOutput={onFollowOutputHandler}
|
||||
firstItemIndex={firstItemIndex}
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
itemContent={itemContent}
|
||||
startReached={fetchMoreMessages}
|
||||
height={"100%"}
|
||||
increaseViewportBy={200}
|
||||
alignToBottom
|
||||
/>
|
||||
<MessageInput />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
20
src/components/Chat/components/ChatDate/ChatDate.tsx
Normal file
20
src/components/Chat/components/ChatDate/ChatDate.tsx
Normal file
@@ -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 (
|
||||
<Center>
|
||||
<Pill size={"md"} className={styles["message"]}>
|
||||
<Box>
|
||||
{date.toLocaleDateString("ru-RU", { day: "numeric", month: "long" })}
|
||||
</Box>
|
||||
</Pill>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDate;
|
||||
15
src/components/Chat/components/ChatFile/ChatFile.module.css
Normal file
15
src/components/Chat/components/ChatFile/ChatFile.module.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.file-circe {
|
||||
@mixin light {
|
||||
background-color: var(--mantine-color-gray-0);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-4);
|
||||
}
|
||||
border-radius: 50%;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
padding: 0.8rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
21
src/components/Chat/components/ChatFile/ChatFile.tsx
Normal file
21
src/components/Chat/components/ChatFile/ChatFile.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { MessageFileSchema } from "../../../../client";
|
||||
import ChatDocument from "./components/ChatDocument.tsx";
|
||||
import ChatPhoto from "./components/ChatPhoto.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
file: MessageFileSchema;
|
||||
}
|
||||
|
||||
const ChatFile = ({ file }: Props) => {
|
||||
if (file.type === "photo") {
|
||||
return (
|
||||
<ChatPhoto file={file} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChatDocument file={file} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatFile;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Center, Group, rem, Stack, Text } from "@mantine/core";
|
||||
import { IconFileFilled } from "@tabler/icons-react";
|
||||
import { MessageFileSchema } from "../../../../../client";
|
||||
import styles from "../ChatFile.module.css";
|
||||
import classNames from "classnames";
|
||||
import downloadFile from "../utils/downloadFile.ts";
|
||||
|
||||
type Props = {
|
||||
file: MessageFileSchema;
|
||||
}
|
||||
|
||||
const ChatDocument = ({ file }: Props) => {
|
||||
const round = (value: number) => Math.round(value * 10) / 10;
|
||||
|
||||
const getFileSize = () => {
|
||||
const BYTES_IN_KB = 1_024;
|
||||
const BYTES_IN_MB = 1_048_576;
|
||||
const BYTES_IN_GB = 1_073_741_824;
|
||||
|
||||
if (file.fileSize < BYTES_IN_KB) {
|
||||
return `${file.fileSize} B`;
|
||||
}
|
||||
if (file.fileSize < BYTES_IN_MB) {
|
||||
return `${round(file.fileSize / BYTES_IN_KB)} KB`;
|
||||
}
|
||||
if (file.fileSize < BYTES_IN_GB) {
|
||||
return `${round(file.fileSize / BYTES_IN_MB)} MB`;
|
||||
}
|
||||
return `${round(file.fileSize / BYTES_IN_GB)} GB`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Group justify={"center"} my={"sm"}>
|
||||
<Center
|
||||
className={classNames(styles["file-circe"])}
|
||||
onClick={() => downloadFile(file)}
|
||||
>
|
||||
<IconFileFilled />
|
||||
</Center>
|
||||
<Stack gap={rem(1)} my={rem(1)}>
|
||||
<Text>{file.fileName}</Text>
|
||||
<Text>{getFileSize()}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatDocument;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { MessageFileSchema } from "../../../../../client";
|
||||
import { Box, Image, Modal } from "@mantine/core";
|
||||
import getDocLink from "../utils/getDocLink.ts";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
|
||||
type Props = {
|
||||
file: MessageFileSchema;
|
||||
}
|
||||
|
||||
const ChatPhoto = ({ file }: Props) => {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
const image = (
|
||||
<Image
|
||||
src={getDocLink(file.id)}
|
||||
radius={"md"}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
size={"auto"}
|
||||
withCloseButton={false}
|
||||
>
|
||||
<Box mah={"90vh"} maw={"90vw"}>
|
||||
{image}
|
||||
</Box>
|
||||
</Modal>
|
||||
<Box
|
||||
my={"sm"}
|
||||
onClick={open}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{image}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatPhoto;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MessageFileSchema } from "../../../../../client";
|
||||
import getDocLink from "./getDocLink.ts";
|
||||
|
||||
|
||||
const downloadFile = (file: MessageFileSchema) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = getDocLink(file.id);
|
||||
link.download = file.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
export default downloadFile;
|
||||
@@ -0,0 +1,5 @@
|
||||
const getDocLink = (fileId: number) => {
|
||||
return `${import.meta.env.VITE_API_URL}/chat/tg-file/${fileId}`;
|
||||
};
|
||||
|
||||
export default getDocLink;
|
||||
115
src/components/Chat/components/Message/Message.tsx
Normal file
115
src/components/Chat/components/Message/Message.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { MessageSchema } from "../../../../client";
|
||||
import { Box, Center, em, Flex, Group, rem } from "@mantine/core";
|
||||
import { formatDateTime } from "../../../../types/utils.ts";
|
||||
import { IconAlertCircle, IconBrandTelegram, IconCheck, IconClock, IconEdit, 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";
|
||||
import ChatFile from "../ChatFile/ChatFile.tsx";
|
||||
|
||||
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 isError = message.status == MessageStatuses.ERROR;
|
||||
|
||||
const {
|
||||
onDeleteMessageClick,
|
||||
onEditMessageClick,
|
||||
onRepeatSendingClick,
|
||||
} = useMessage();
|
||||
|
||||
const { showContextMenu } = useContextMenu();
|
||||
|
||||
const contextMenuSuccessMsg = () => showContextMenu([
|
||||
{
|
||||
key: "delete",
|
||||
onClick: () => onDeleteMessageClick(message),
|
||||
title: "Удалить",
|
||||
icon: <IconTrash />,
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
onClick: () => onEditMessageClick(message),
|
||||
title: "Редактировать",
|
||||
icon: <IconEdit />,
|
||||
},
|
||||
]);
|
||||
|
||||
const contextMenuErrorMsg = () => showContextMenu([
|
||||
{
|
||||
key: "repeatSending",
|
||||
onClick: () => onRepeatSendingClick(message),
|
||||
title: "Повторить отправку",
|
||||
icon: <IconBrandTelegram />,
|
||||
},
|
||||
]);
|
||||
|
||||
const getContext = () => {
|
||||
if (!isMine || message.file) return;
|
||||
|
||||
if (isSuccess) return contextMenuSuccessMsg();
|
||||
if (isError) return contextMenuErrorMsg();
|
||||
};
|
||||
|
||||
const getStatusIcon = () => {
|
||||
const size = em(18);
|
||||
if (message.status == MessageStatuses.ERROR) {
|
||||
return <IconAlertCircle size={size} color={"red"}/>;
|
||||
}
|
||||
if (message.status == MessageStatuses.SENDING) {
|
||||
return <IconClock size={size} />;
|
||||
}
|
||||
return <IconCheck size={size} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex justify={isMine ? "flex-end" : "flex-start"}>
|
||||
<Box
|
||||
py={rem(5)}
|
||||
px={rem(15)}
|
||||
className={classNames(isMine ? styles["message"] : styles["other-message"])}
|
||||
onContextMenu={getContext()}
|
||||
maw={em(600)}
|
||||
>
|
||||
{!isMine && (
|
||||
<div>{message.tgSender!.lastName} {message.tgSender!.firstName}</div>
|
||||
)}
|
||||
{message.file && (
|
||||
<Group>
|
||||
<ChatFile file={message.file} />
|
||||
</Group>
|
||||
)}
|
||||
<div>{message.text}</div>
|
||||
<Group
|
||||
gap={em(5)}
|
||||
justify={"flex-end"}
|
||||
align={"center"}
|
||||
wrap={"nowrap"}
|
||||
>
|
||||
{message.isEdited && "ред."}
|
||||
<Center>
|
||||
{formatDateTime(message.createdAt).substring(11, 16)}
|
||||
</Center>
|
||||
{isMine && (
|
||||
<Center>
|
||||
{getStatusIcon()}
|
||||
</Center>
|
||||
)}
|
||||
</Group>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Message;
|
||||
48
src/components/Chat/components/Message/hooks/useMessage.tsx
Normal file
48
src/components/Chat/components/Message/hooks/useMessage.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ChatService, MessageSchema } from "../../../../../client";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { useChatContext } from "../../../../../pages/ClientsPage/contexts/ChatContext.tsx";
|
||||
|
||||
const useMessage = () => {
|
||||
const { form } = useChatContext();
|
||||
|
||||
const onDeleteMessageClick = (message: MessageSchema) => {
|
||||
ChatService.deleteMessage({
|
||||
messageId: message.id,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
const onEditMessageClick = (message: MessageSchema) => {
|
||||
form.setValues({
|
||||
messageId: message.id,
|
||||
message: message.text,
|
||||
});
|
||||
};
|
||||
|
||||
const onRepeatSendingClick = (message: MessageSchema) => {
|
||||
ChatService.repeatSendingTextMessage({
|
||||
requestBody: {
|
||||
message,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
return {
|
||||
onDeleteMessageClick,
|
||||
onEditMessageClick,
|
||||
onRepeatSendingClick,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMessage;
|
||||
75
src/components/Chat/components/MessageInput/MessageInput.tsx
Normal file
75
src/components/Chat/components/MessageInput/MessageInput.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ActionIcon, Button, Divider, Group, Stack, Textarea, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconPaperclip, IconSend2, IconX } from "@tabler/icons-react";
|
||||
import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
|
||||
import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx";
|
||||
import SelectedFile from "../SelectedFile/SelectedFile.tsx";
|
||||
|
||||
|
||||
const MessageInput = () => {
|
||||
const {
|
||||
submitMessage,
|
||||
chat,
|
||||
form,
|
||||
files,
|
||||
fileDialog,
|
||||
isMessageSending,
|
||||
} = useChatContext();
|
||||
|
||||
const getFiles = files.map(file => (
|
||||
<SelectedFile key={file.name} file={file} />
|
||||
));
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => submitMessage(values))}>
|
||||
<Stack gap={"xs"}>
|
||||
<Divider />
|
||||
{getFiles}
|
||||
<Group wrap={"nowrap"} align={"flex-end"}>
|
||||
{chat?.tgGroup?.tgInviteLink && (
|
||||
<ActionIconCopy
|
||||
onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"}
|
||||
value={chat.tgGroup.tgInviteLink}
|
||||
/>
|
||||
)}
|
||||
<Tooltip label={"Прикрепить файлы"}>
|
||||
<ActionIcon
|
||||
variant={"default"}
|
||||
onClick={fileDialog.open}
|
||||
size="lg"
|
||||
>
|
||||
<IconPaperclip />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Textarea
|
||||
{...form.getInputProps("message")}
|
||||
w={"100%"}
|
||||
minRows={1}
|
||||
maxRows={4}
|
||||
autosize
|
||||
/>
|
||||
{form.values.messageId && (
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={form.reset}
|
||||
>
|
||||
<IconX />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={"default"}
|
||||
type="submit"
|
||||
disabled={isMessageSending}
|
||||
>
|
||||
{form.values.messageId ? (
|
||||
<IconCheck />
|
||||
) : (
|
||||
<IconSend2 />
|
||||
)}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessageInput;
|
||||
38
src/components/Chat/components/SelectedFile/SelectedFile.tsx
Normal file
38
src/components/Chat/components/SelectedFile/SelectedFile.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
|
||||
|
||||
type Props = {
|
||||
file: File;
|
||||
}
|
||||
|
||||
const SelectedFile = ({ file }: Props) => {
|
||||
const { files, filesHandlers } = useChatContext();
|
||||
|
||||
const onCancelFileClick = () => {
|
||||
const idx = files.findIndex(f => f.name === file.name && f.type === file.type);
|
||||
|
||||
if (idx < 0) return;
|
||||
filesHandlers.remove(idx);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Group justify={"space-between"}>
|
||||
<Text>
|
||||
{file.name}
|
||||
</Text>
|
||||
<Box
|
||||
onClick={onCancelFileClick}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<IconX />
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectedFile;
|
||||
@@ -3,13 +3,16 @@ import CreateServiceCategoryModal from "../pages/ServicesPage/modals/CreateServi
|
||||
import CreateServiceModal from "../pages/ServicesPage/modals/CreateServiceModal.tsx";
|
||||
import createProductModal from "../pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx";
|
||||
import ProductFormModal from "../pages/ClientsPage/modals/ClientFormModal/ClientFormModal.tsx";
|
||||
import AddCardServiceModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardServiceModal.tsx";
|
||||
import AddCardProductModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardProductModal.tsx";
|
||||
import AddCardServiceModal
|
||||
from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardServiceModal.tsx";
|
||||
import AddCardProductModal
|
||||
from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardProductModal.tsx";
|
||||
import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
|
||||
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
|
||||
import BarcodeTemplateFormModal
|
||||
from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
|
||||
import ProductServiceFormModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/ProductServiceFormModal.tsx";
|
||||
import ProductServiceFormModal
|
||||
from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/ProductServiceFormModal.tsx";
|
||||
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
|
||||
import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx";
|
||||
import EmployeeTableModal from "./EmployeeTableModal/EmployeeTableModal.tsx";
|
||||
|
||||
34
src/modules/cardModules/cardEditorTabs/ChatTab/ChatTab.tsx
Normal file
34
src/modules/cardModules/cardEditorTabs/ChatTab/ChatTab.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
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, isRequestSending } = useChatTab();
|
||||
|
||||
if (!selectedCard?.chat) {
|
||||
return (
|
||||
<Stack>
|
||||
<InlineButton
|
||||
onClick={onChatCreateClick}
|
||||
disabled={isRequestSending}
|
||||
>
|
||||
<IconMessagePlus />
|
||||
Создать чат
|
||||
</InlineButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatContextProvider>
|
||||
<ChatWrapper />
|
||||
</ChatContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatTab;
|
||||
@@ -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 (
|
||||
<Chat />
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatWrapper;
|
||||
@@ -0,0 +1,54 @@
|
||||
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";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
const useChatTab = () => {
|
||||
const { selectedCard, refetchCard } = useCardPageContext();
|
||||
const [isRequestSending, setIsRequestSending] = useState<boolean>(false);
|
||||
|
||||
const createChat = () => {
|
||||
if (!selectedCard?.clientId) {
|
||||
notifications.error({ message: "Ошибка создания чата, не указан клиент" });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRequestSending(true);
|
||||
ChatService.createChat({
|
||||
requestBody: {
|
||||
clientId: selectedCard.clientId,
|
||||
cardId: selectedCard.id,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
refetchCard();
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
.finally(() => setIsRequestSending(false));
|
||||
};
|
||||
|
||||
const onChatCreateClick = () => {
|
||||
modals.openConfirmModal({
|
||||
title: "Создание чата",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите создать чат для сделки {selectedCard?.name}?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "green" },
|
||||
onConfirm: createChat,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
onChatCreateClick,
|
||||
isRequestSending,
|
||||
};
|
||||
};
|
||||
|
||||
export default useChatTab;
|
||||
@@ -146,16 +146,18 @@ const ClientTab = () => {
|
||||
value={client}
|
||||
onChange={setClient}
|
||||
withLabel
|
||||
disabled={!isEqual(initialValues, form.values)}
|
||||
disabled={!isEqual(initialValues, form.values) || !!card?.chat}
|
||||
/>
|
||||
<Group>
|
||||
<InlineButton
|
||||
onClick={handleSelectClient}
|
||||
disabled={!isEditorDisabled()}
|
||||
>
|
||||
Сохранить
|
||||
</InlineButton>
|
||||
</Group>
|
||||
{!card?.chat && (
|
||||
<Group>
|
||||
<InlineButton
|
||||
onClick={handleSelectClient}
|
||||
disabled={!isEditorDisabled()}
|
||||
>
|
||||
Сохранить
|
||||
</InlineButton>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
{clientDataEditor}
|
||||
|
||||
@@ -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 = <EmployeesTab />;
|
||||
modules[ModuleNames.SHIPMENT].tab = <ShippingTab />;
|
||||
modules[ModuleNames.MANAGERS].tab = <ManagerTab />;
|
||||
modules[ModuleNames.CHAT].tab = <ChatTab />;
|
||||
|
||||
return modules;
|
||||
};
|
||||
|
||||
@@ -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: <IconUserCog />,
|
||||
}
|
||||
},
|
||||
[ModuleNames.CHAT]: {
|
||||
info: {
|
||||
label: "Чат",
|
||||
key: "chat",
|
||||
icon: <IconMessage />,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const MODULES = connectModules(modules);
|
||||
|
||||
@@ -64,7 +64,7 @@ const generateRows = (modules: Module[]) => {
|
||||
};
|
||||
const tsxContent = template(data);
|
||||
fs.writeFileSync(OUTPUT_PATH, tsxContent);
|
||||
console.log("File successfully generated.");
|
||||
console.log("ChatFile successfully generated.");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
</PageBlock>
|
||||
<PageBlock>
|
||||
<ClientsTable
|
||||
onChange={onChange}
|
||||
onDelete={onDelete}
|
||||
items={clients}
|
||||
/>
|
||||
<ChatContextProvider>
|
||||
<ClientsTable
|
||||
onChange={onChange}
|
||||
onDelete={onDelete}
|
||||
items={clients}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<ClientChatDrawer />
|
||||
</ChatContextProvider>
|
||||
</PageBlock>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
client: ClientSchema;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
const ClientChatButton = ({ client, refetch }: Props) => {
|
||||
const { setChat } = useChatContext();
|
||||
const [isRequestSending, setIsRequestSending] = useState<boolean>(false);
|
||||
|
||||
const createChat = () => {
|
||||
setIsRequestSending(true);
|
||||
ChatService.createChat({
|
||||
requestBody: {
|
||||
clientId: client.id,
|
||||
cardId: null,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
refetch();
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
.finally(() => setIsRequestSending(false));
|
||||
};
|
||||
|
||||
const onCreateChatClick = () => {
|
||||
modals.openConfirmModal({
|
||||
title: "Создание чата",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите создать чат с {client.name}?
|
||||
</Text>
|
||||
),
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "green" },
|
||||
onConfirm: createChat,
|
||||
});
|
||||
};
|
||||
|
||||
if (client.chat) {
|
||||
return (
|
||||
<Tooltip label="Открыть чат">
|
||||
<ActionIcon
|
||||
onClick={() => setChat(client.chat!)}
|
||||
variant={"default"}>
|
||||
<IconMessage />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip label="Создать чат">
|
||||
<ActionIcon
|
||||
onClick={onCreateChatClick}
|
||||
variant={"default"}
|
||||
disabled={isRequestSending}
|
||||
>
|
||||
<IconMessagePlus />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientChatButton;
|
||||
@@ -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<CRUDTableProps<ClientSchema>> = ({
|
||||
items,
|
||||
onDelete,
|
||||
onChange,
|
||||
}) => {
|
||||
type RefetchProps = {
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
const ClientsTable: FC<CRUDTableProps<ClientSchema> & RefetchProps> = ({
|
||||
items,
|
||||
onDelete,
|
||||
onChange,
|
||||
refetch,
|
||||
}) => {
|
||||
const columns = useClientsTableColumns();
|
||||
|
||||
const onEditClick = (client: ClientSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
@@ -26,6 +33,7 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseTable
|
||||
@@ -38,6 +46,7 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<ClientChatButton client={row.original} refetch={refetch}/>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
@@ -50,8 +59,7 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
if (onDelete)
|
||||
onDelete(row.original);
|
||||
if (onDelete) onDelete(row.original);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
|
||||
234
src/pages/ClientsPage/contexts/ChatContext.tsx
Normal file
234
src/pages/ClientsPage/contexts/ChatContext.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React, { createContext, FC, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { ChatSchema, ChatService, MessageSchema } from "../../../client";
|
||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||
import { useForm, UseFormReturnType } from "@mantine/form";
|
||||
import { useFileDialog, useListState, UseListStateHandlers } from "@mantine/hooks";
|
||||
|
||||
export type MessageForm = {
|
||||
message: string;
|
||||
messageId?: number;
|
||||
}
|
||||
|
||||
type FileDialog = {
|
||||
files: FileList | null;
|
||||
open: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
type ChatContextState = {
|
||||
chat: ChatSchema | null;
|
||||
setChat: (chat: ChatSchema | null) => void;
|
||||
messages: MessageSchema[];
|
||||
lastMessage: MessageSchema | null;
|
||||
firstItemIndex: number;
|
||||
form: UseFormReturnType<MessageForm>;
|
||||
|
||||
submitMessage: (values: MessageForm) => void;
|
||||
fetchMoreMessages: () => void;
|
||||
files: Array<File>;
|
||||
filesHandlers: UseListStateHandlers<File>;
|
||||
fileDialog: FileDialog;
|
||||
isMessageSending: boolean;
|
||||
};
|
||||
|
||||
const ChatContext = createContext<ChatContextState | undefined>(undefined);
|
||||
|
||||
const useChatContextState = () => {
|
||||
const [chat, setChat] = useState<ChatSchema | null>(null);
|
||||
const [messages, setMessages] = useState<MessageSchema[]>([]);
|
||||
const lastMessage = messages?.length ? messages[messages?.length - 1] : null;
|
||||
const [firstItemIndex, setFirstItemIndex] = useState(10000);
|
||||
const limit: number = 30;
|
||||
let offset: number = 0;
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
|
||||
const [files, filesHandlers] = useListState<File>([]);
|
||||
const fileDialog = useFileDialog();
|
||||
const [isMessageSending, setIsMessageSending] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
filesHandlers.append(...Array.from(fileDialog.files ?? []));
|
||||
}, [fileDialog.files]);
|
||||
|
||||
const form = useForm<MessageForm>({
|
||||
initialValues: {
|
||||
message: "",
|
||||
},
|
||||
});
|
||||
|
||||
const setChatValue = (chat: ChatSchema | null) => {
|
||||
if (chat) {
|
||||
setFirstItemIndex(10000);
|
||||
offset = 0;
|
||||
setHasMore(true);
|
||||
filesHandlers.setState([]);
|
||||
form.reset();
|
||||
}
|
||||
setChat(chat);
|
||||
};
|
||||
|
||||
const fetchMoreMessages = useCallback(() => {
|
||||
return setTimeout(() => {
|
||||
if (!chat || !hasMore) return;
|
||||
|
||||
offset += limit;
|
||||
|
||||
ChatService.getMessages({
|
||||
requestBody: {
|
||||
chatId: chat.id,
|
||||
offset,
|
||||
limit,
|
||||
},
|
||||
})
|
||||
.then(({ messages: newMessages }) => {
|
||||
setFirstItemIndex(prev => prev - newMessages.length);
|
||||
setMessages(prev => [...newMessages.reverse(), ...prev]);
|
||||
setHasMore(newMessages.length === limit);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
}, 500);
|
||||
}, [setMessages, chat, offset, hasMore, setHasMore]);
|
||||
|
||||
const fetchMessages = () => {
|
||||
if (!chat) return;
|
||||
|
||||
ChatService.getMessages({
|
||||
requestBody: {
|
||||
chatId: chat.id,
|
||||
offset: 0,
|
||||
limit: limit + offset,
|
||||
},
|
||||
})
|
||||
.then(({ messages: newMessages }) => {
|
||||
setMessages(newMessages.reverse());
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMessages();
|
||||
|
||||
const interval = setInterval(fetchMessages, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [chat]);
|
||||
|
||||
const sendMessageWithFiles = (values: MessageForm) => {
|
||||
if (!chat) return;
|
||||
setIsMessageSending(true);
|
||||
|
||||
ChatService.sendMessagesWithFiles({
|
||||
formData: {
|
||||
files,
|
||||
},
|
||||
chatId: chat.id,
|
||||
caption: values.message,
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
return;
|
||||
}
|
||||
filesHandlers.setState([]);
|
||||
form.reset();
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
.finally(() => setIsMessageSending(false));
|
||||
};
|
||||
|
||||
const sendTextMessage = (values: MessageForm) => {
|
||||
if (!chat) return;
|
||||
setIsMessageSending(true);
|
||||
|
||||
ChatService.sendTextMessage({
|
||||
requestBody: {
|
||||
message: {
|
||||
text: values.message,
|
||||
chatId: chat.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
form.reset();
|
||||
setIsMessageSending(false);
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
.finally(() => setIsMessageSending(false));
|
||||
};
|
||||
|
||||
const editMessage = (values: MessageForm) => {
|
||||
if (!chat) return;
|
||||
setIsMessageSending(true);
|
||||
|
||||
ChatService.editMessage({
|
||||
requestBody: {
|
||||
message: {
|
||||
id: values.messageId!,
|
||||
text: values.message,
|
||||
chatId: chat.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.error({ message });
|
||||
}
|
||||
form.reset();
|
||||
setIsMessageSending(false);
|
||||
})
|
||||
.catch(err => console.log(err))
|
||||
.finally(() => setIsMessageSending(false));
|
||||
};
|
||||
|
||||
const submitMessage = (values: MessageForm) => {
|
||||
if (values.messageId) {
|
||||
editMessage(values);
|
||||
} else if (files.length === 0) {
|
||||
sendTextMessage(values);
|
||||
} else {
|
||||
sendMessageWithFiles(values);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
chat,
|
||||
setChat: setChatValue,
|
||||
messages,
|
||||
lastMessage,
|
||||
firstItemIndex,
|
||||
form,
|
||||
|
||||
submitMessage,
|
||||
fetchMoreMessages,
|
||||
files,
|
||||
filesHandlers,
|
||||
fileDialog,
|
||||
isMessageSending,
|
||||
};
|
||||
};
|
||||
|
||||
type ChatContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const ChatContextProvider: FC<ChatContextProviderProps> = ({ children }) => {
|
||||
const state = useChatContextState();
|
||||
return (
|
||||
<ChatContext.Provider value={state}>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useChatContext = () => {
|
||||
const context = useContext(ChatContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useChatContext must be used within a ChatContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
24
src/pages/ClientsPage/drawers/ClientChatDrawer.tsx
Normal file
24
src/pages/ClientsPage/drawers/ClientChatDrawer.tsx
Normal file
@@ -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 (
|
||||
<Drawer
|
||||
opened={!!chat}
|
||||
onClose={() => setChat(null)}
|
||||
position={"right"}
|
||||
size={"calc(50vw)"}
|
||||
withCloseButton={false}
|
||||
>
|
||||
<Chat />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientChatDrawer;
|
||||
Reference in New Issue
Block a user