diff --git a/frontend/src/app/messages-chatscope/page.tsx b/frontend/src/app/messages-chatscope/page.tsx index a17bc99c..0a680ec8 100644 --- a/frontend/src/app/messages-chatscope/page.tsx +++ b/frontend/src/app/messages-chatscope/page.tsx @@ -20,6 +20,7 @@ import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css'; import { AppShell } from '@/components/AppShell'; import { apiClient } from '@/lib/api'; import { useUser } from '@/contexts/UserContext'; +import NewChatModal from '@/components/NewChatModal'; import type { Chat as ChatType, Message as MessageType } from '@/types/api'; const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "https://corp.robotail.pro"; @@ -96,6 +97,7 @@ export default function ChatscapeMessenger() { const [sending, setSending] = useState(false); const [typing, setTyping] = useState(false); const [typingTimeout, setTypingTimeout] = useState(null); + const [isNewChatModalOpen, setIsNewChatModalOpen] = useState(false); const wsRef = useRef(null); @@ -122,6 +124,21 @@ export default function ChatscapeMessenger() { loadChats(); }, []); + // Обработчик создания нового чата + const handleChatCreated = async (chatId: number) => { + try { + // Перезагружаем список чатов + const response = await apiClient.getChats(); + const chatsList = response.results || []; + setChats(chatsList); + + // Открываем новый чат + setSelectedChatId(chatId); + } catch (error) { + console.error('Error reloading chats:', error); + } + }; + // Загрузка сообщений выбранного чата useEffect(() => { if (!selectedChatId) return; @@ -282,6 +299,29 @@ export default function ChatscapeMessenger() { {/* Боковая панель со списком чатов */} + {/* Кнопка Новый чат */} +
+ +
+ + + {/* Модальное окно для создания нового чата */} + setIsNewChatModalOpen(false)} + onChatCreated={handleChatCreated} + /> ); } diff --git a/frontend/src/components/NewChatModal.tsx b/frontend/src/components/NewChatModal.tsx new file mode 100644 index 00000000..4083937b --- /dev/null +++ b/frontend/src/components/NewChatModal.tsx @@ -0,0 +1,265 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { X, Search, Check, Users } from "lucide-react"; +import apiClient from "@/lib/api"; + +interface Employee { + id: number; + first_name: string; + last_name: string; + middle_name?: string; + avatar?: string; + position?: string; + department?: { + id: number; + name: string; + }; +} + +interface NewChatModalProps { + isOpen: boolean; + onClose: () => void; + onChatCreated: (chatId: number) => void; +} + +export default function NewChatModal({ isOpen, onClose, onChatCreated }: NewChatModalProps) { + const [employees, setEmployees] = useState([]); + const [selectedEmployees, setSelectedEmployees] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [chatName, setChatName] = useState(""); + const [loading, setLoading] = useState(false); + const [creating, setCreating] = useState(false); + + // Загрузка сотрудников + useEffect(() => { + if (isOpen) { + loadEmployees(); + } + }, [isOpen, searchQuery]); + + const loadEmployees = async () => { + setLoading(true); + try { + const response = await apiClient.getEmployees({ search: searchQuery }); + setEmployees(response.results || response); + } catch (error) { + console.error("Failed to load employees:", error); + } finally { + setLoading(false); + } + }; + + const toggleEmployee = (employeeId: number) => { + setSelectedEmployees(prev => + prev.includes(employeeId) + ? prev.filter(id => id !== employeeId) + : [...prev, employeeId] + ); + }; + + const handleCreateChat = async () => { + if (selectedEmployees.length === 0) { + alert("Выберите хотя бы одного участника"); + return; + } + + setCreating(true); + try { + // Определяем тип чата (приватный или групповой) + const isGroup = selectedEmployees.length > 1; + const chatType = isGroup ? "group" : "private"; + + const chatData: any = { + type: chatType, + participants: selectedEmployees, + }; + + // Для групповых чатов добавляем название + if (isGroup && chatName.trim()) { + chatData.name = chatName.trim(); + } + + const newChat = await apiClient.createChat(chatData); + + // Уведомляем родительский компонент о новом чате + onChatCreated(newChat.id); + + // Сбрасываем форму + setSelectedEmployees([]); + setChatName(""); + setSearchQuery(""); + onClose(); + } catch (error) { + console.error("Failed to create chat:", error); + alert("Не удалось создать чат. Попробуйте еще раз."); + } finally { + setCreating(false); + } + }; + + const getEmployeeFullName = (emp: Employee) => { + return `${emp.last_name} ${emp.first_name} ${emp.middle_name || ""}`.trim(); + }; + + if (!isOpen) return null; + + const isGroup = selectedEmployees.length > 1; + + return ( +
+
+ {/* Header */} +
+
+ +

Новый чат

+
+ +
+ + {/* Chat Name (for groups) */} + {isGroup && ( +
+ setChatName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ )} + + {/* Selected Employees */} + {selectedEmployees.length > 0 && ( +
+
+ Выбрано: {selectedEmployees.length} +
+
+ {employees + .filter(emp => selectedEmployees.includes(emp.id)) + .map(emp => ( +
+ {getEmployeeFullName(emp)} + +
+ ))} +
+
+ )} + + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ + {/* Employee List */} +
+ {loading ? ( +
+ Загрузка сотрудников... +
+ ) : employees.length === 0 ? ( +
+ Сотрудники не найдены +
+ ) : ( +
+ {employees.map((emp) => { + const isSelected = selectedEmployees.includes(emp.id); + return ( + + ); + })} +
+ )} +
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index dcb9d926..85c27145 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -768,6 +768,169 @@ class ApiClient { body: JSON.stringify({ emoji }), }); } + + // ============= Communications (Chats & Messages) API ============= + + /** + * Получить список чатов пользователя + */ + async getChats(params?: { search?: string; page?: number }): Promise { + const queryParams = new URLSearchParams(); + if (params?.search) queryParams.append('search', params.search); + if (params?.page) queryParams.append('page', params.page.toString()); + + const query = queryParams.toString(); + return this.request(`/api/v1/communications/chats/${query ? '?' + query : ''}`); + } + + /** + * Получить детали чата по ID + */ + async getChat(chatId: number): Promise { + return this.request(`/api/v1/communications/chats/${chatId}/`); + } + + /** + * Создать новый чат + * type: 'private' | 'group' | 'department' | 'general' + */ + async createChat(data: { + type: string; + participants?: number[]; + name?: string; + description?: string; + department?: number; + include_all_employees?: boolean; + }): Promise { + return this.request('/api/v1/communications/chats/', { + method: 'POST', + body: JSON.stringify(data), + }); + } + + /** + * Получить сообщения чата с пагинацией + */ + async getChatMessages( + chatId: number, + params?: { + limit?: number; + before_id?: number; + after_id?: number; + before?: string; + after?: string; + } + ): Promise { + const queryParams = new URLSearchParams(); + if (params?.limit) queryParams.append('limit', params.limit.toString()); + if (params?.before_id) queryParams.append('before_id', params.before_id.toString()); + if (params?.after_id) queryParams.append('after_id', params.after_id.toString()); + if (params?.before) queryParams.append('before', params.before); + if (params?.after) queryParams.append('after', params.after); + + const query = queryParams.toString(); + return this.request(`/api/v1/communications/chats/${chatId}/messages/${query ? '?' + query : ''}`); + } + + /** + * Отправить текстовое сообщение + */ + async sendMessage( + chatId: number, + content: string, + replyTo?: number + ): Promise { + return this.request('/api/v1/communications/messages/', { + method: 'POST', + body: JSON.stringify({ + chat_id: chatId, + content, + reply_to: replyTo, + }), + }); + } + + /** + * Отправить сообщение с файлами + */ + async sendMessageWithFiles( + chatId: number, + content: string, + files: File[], + replyTo?: number + ): Promise { + const formData = new FormData(); + formData.append('chat_id', chatId.toString()); + formData.append('content', content); + if (replyTo) formData.append('reply_to', replyTo.toString()); + + // Добавляем файлы с префиксом file_ + files.forEach((file, index) => { + formData.append(`file_${index}`, file); + }); + + const token = this.getToken(); + const response = await fetch('/api/v1/communications/messages/upload/', { + method: 'POST', + headers: token ? { 'Authorization': `Bearer ${token}` } : {}, + body: formData, + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.status} ${response.statusText}`); + } + + return response.json(); + } + + /** + * Редактировать сообщение + */ + async updateMessage(messageId: number, content: string): Promise { + return this.request(`/api/v1/communications/messages/${messageId}/`, { + method: 'PATCH', + body: JSON.stringify({ content }), + }); + } + + /** + * Удалить сообщение + */ + async deleteMessage(messageId: number): Promise { + await this.request(`/api/v1/communications/messages/${messageId}/`, { + method: 'DELETE', + }); + } + + /** + * Пометить чат как прочитанный + */ + async markChatAsRead(chatId: number, lastReadMessageId?: number): Promise { + return this.request(`/api/v1/communications/chats/${chatId}/mark_read/`, { + method: 'POST', + body: JSON.stringify({ + last_read_message_id: lastReadMessageId + }), + }); + } + + /** + * Закрепить/открепить чат + */ + async togglePinChat(chatId: number): Promise { + return this.request(`/api/v1/communications/chats/${chatId}/pin/`, { + method: 'POST', + }); + } + + /** + * Включить/выключить уведомления для чата + */ + async toggleChatNotifications(chatId: number): Promise { + return this.request(`/api/v1/communications/chats/${chatId}/notifications/`, { + method: 'POST', + }); + } } export const apiClient = new ApiClient();