diff --git a/frontend/src/components/CreateTicketModal.jsx b/frontend/src/components/CreateTicketModal.jsx
new file mode 100644
index 0000000..e38521b
--- /dev/null
+++ b/frontend/src/components/CreateTicketModal.jsx
@@ -0,0 +1,93 @@
+import { useState } from 'react';
+import { X } from 'lucide-react';
+import axios from 'axios';
+import { API_URL } from '../config';
+
+export default function CreateTicketModal({ token, theme, onClose, onCreated }) {
+ const [formData, setFormData] = useState({
+ title: '',
+ description: ''
+ });
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+
+ try {
+ await axios.post(
+ `${API_URL}/api/tickets/create`,
+ formData,
+ { headers: { Authorization: `Bearer ${token}` } }
+ );
+ onCreated();
+ } catch (error) {
+ alert(error.response?.data?.detail || 'Ошибка создания тикета');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
Создать тикет
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/TicketChat.jsx b/frontend/src/components/TicketChat.jsx
new file mode 100644
index 0000000..c018ead
--- /dev/null
+++ b/frontend/src/components/TicketChat.jsx
@@ -0,0 +1,241 @@
+import { useState, useEffect, useRef } from 'react';
+import { ArrowLeft, Send, Clock, AlertCircle, CheckCircle } from 'lucide-react';
+import axios from 'axios';
+import { API_URL } from '../config';
+
+export default function TicketChat({ ticket, token, user, theme, onBack }) {
+ const [messages, setMessages] = useState(ticket.messages || []);
+ const [newMessage, setNewMessage] = useState('');
+ const [currentTicket, setCurrentTicket] = useState(ticket);
+ const [loading, setLoading] = useState(false);
+ const messagesEndRef = useRef(null);
+
+ useEffect(() => {
+ scrollToBottom();
+ const interval = setInterval(loadTicket, 3000);
+ return () => clearInterval(interval);
+ }, []);
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages]);
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ const loadTicket = async () => {
+ try {
+ const { data } = await axios.get(`${API_URL}/api/tickets/${ticket.id}`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setCurrentTicket(data);
+ setMessages(data.messages || []);
+ } catch (error) {
+ console.error('Ошибка загрузки тикета:', error);
+ }
+ };
+
+ const sendMessage = async (e) => {
+ e.preventDefault();
+ if (!newMessage.trim() || loading) return;
+
+ setLoading(true);
+ try {
+ const { data } = await axios.post(
+ `${API_URL}/api/tickets/${ticket.id}/message`,
+ { text: newMessage.trim() },
+ { headers: { Authorization: `Bearer ${token}` } }
+ );
+ setMessages(data.ticket.messages);
+ setCurrentTicket(data.ticket);
+ setNewMessage('');
+ } catch (error) {
+ console.error('Ошибка отправки сообщения:', error);
+ alert(error.response?.data?.detail || 'Ошибка отправки сообщения');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const changeStatus = async (newStatus) => {
+ try {
+ const { data } = await axios.put(
+ `${API_URL}/api/tickets/${ticket.id}/status`,
+ { status: newStatus },
+ { headers: { Authorization: `Bearer ${token}` } }
+ );
+ setCurrentTicket(data.ticket);
+ setMessages(data.ticket.messages);
+ } catch (error) {
+ console.error('Ошибка изменения статуса:', error);
+ alert(error.response?.data?.detail || 'Ошибка изменения статуса');
+ }
+ };
+
+ const getStatusIcon = (status) => {
+ switch (status) {
+ case 'pending':
+ return ;
+ case 'in_progress':
+ return ;
+ case 'closed':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusText = (status) => {
+ switch (status) {
+ case 'pending':
+ return 'На рассмотрении';
+ case 'in_progress':
+ return 'В работе';
+ case 'closed':
+ return 'Закрыт';
+ default:
+ return status;
+ }
+ };
+
+ const getStatusColor = (status) => {
+ switch (status) {
+ case 'pending':
+ return 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50';
+ case 'in_progress':
+ return 'bg-blue-500/20 text-blue-500 border-blue-500/50';
+ case 'closed':
+ return 'bg-green-500/20 text-green-500 border-green-500/50';
+ default:
+ return 'bg-gray-500/20 text-gray-500 border-gray-500/50';
+ }
+ };
+
+ const canChangeStatus = user.role === 'admin' || user.role === 'support';
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
{currentTicket.title}
+
+ Автор: {currentTicket.author} • Создан: {new Date(currentTicket.created_at).toLocaleString('ru-RU')}
+
+
+
+ {getStatusIcon(currentTicket.status)}
+ {getStatusText(currentTicket.status)}
+
+
+
+ {/* Status Controls */}
+ {canChangeStatus && (
+
+
+
+
+
+ )}
+
+
+ {/* Messages */}
+
+ {messages.map((msg, index) => (
+
+
+ {msg.author !== 'system' && msg.author !== user.username && (
+
+ {msg.author}
+
+ )}
+
{msg.text}
+
+ {new Date(msg.timestamp).toLocaleTimeString('ru-RU')}
+
+
+
+ ))}
+
+
+
+ {/* Input */}
+ {currentTicket.status !== 'closed' && (
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/Tickets.jsx b/frontend/src/components/Tickets.jsx
new file mode 100644
index 0000000..af4ca9c
--- /dev/null
+++ b/frontend/src/components/Tickets.jsx
@@ -0,0 +1,179 @@
+import { useState, useEffect } from 'react';
+import { MessageSquare, Plus, Clock, CheckCircle, AlertCircle } from 'lucide-react';
+import axios from 'axios';
+import { API_URL } from '../config';
+import TicketChat from './TicketChat';
+import CreateTicketModal from './CreateTicketModal';
+
+export default function Tickets({ token, user, theme }) {
+ const [tickets, setTickets] = useState([]);
+ const [selectedTicket, setSelectedTicket] = useState(null);
+ const [showCreateModal, setShowCreateModal] = useState(false);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ loadTickets();
+ const interval = setInterval(loadTickets, 5000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const loadTickets = async () => {
+ try {
+ const { data } = await axios.get(`${API_URL}/api/tickets`, {
+ headers: { Authorization: `Bearer ${token}` }
+ });
+ setTickets(data);
+ setLoading(false);
+ } catch (error) {
+ console.error('Ошибка загрузки тикетов:', error);
+ setLoading(false);
+ }
+ };
+
+ const handleTicketCreated = () => {
+ setShowCreateModal(false);
+ loadTickets();
+ };
+
+ const getStatusIcon = (status) => {
+ switch (status) {
+ case 'pending':
+ return ;
+ case 'in_progress':
+ return ;
+ case 'closed':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getStatusText = (status) => {
+ switch (status) {
+ case 'pending':
+ return 'На рассмотрении';
+ case 'in_progress':
+ return 'В работе';
+ case 'closed':
+ return 'Закрыт';
+ default:
+ return status;
+ }
+ };
+
+ const getStatusColor = (status) => {
+ switch (status) {
+ case 'pending':
+ return 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50';
+ case 'in_progress':
+ return 'bg-blue-500/20 text-blue-500 border-blue-500/50';
+ case 'closed':
+ return 'bg-green-500/20 text-green-500 border-green-500/50';
+ default:
+ return 'bg-gray-500/20 text-gray-500 border-gray-500/50';
+ }
+ };
+
+ if (selectedTicket) {
+ return (
+ {
+ setSelectedTicket(null);
+ loadTickets();
+ }}
+ />
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
Тикеты
+
Система поддержки
+
+
+
+
+ {/* Tickets List */}
+ {loading ? (
+
+
+
Загрузка тикетов...
+
+ ) : tickets.length === 0 ? (
+
+
+
Нет тикетов
+
+ Создайте первый тикет для обращения в поддержку
+
+
+
+ ) : (
+
+ {tickets.map((ticket) => (
+
setSelectedTicket(ticket)}
+ className={`${theme.card} ${theme.border} border rounded-2xl p-6 cursor-pointer ${theme.hover} transition-all duration-200`}
+ >
+
+
+
{ticket.title}
+
+ {ticket.description}
+
+
+
+ {getStatusIcon(ticket.status)}
+ {getStatusText(ticket.status)}
+
+
+
+
+ Автор: {ticket.author}
+
+ •
+
+ Сообщений: {ticket.messages?.length || 0}
+
+ •
+
+ {new Date(ticket.created_at).toLocaleString('ru-RU')}
+
+
+
+ ))}
+
+ )}
+
+
+ {showCreateModal && (
+
setShowCreateModal(false)}
+ onCreated={handleTicketCreated}
+ />
+ )}
+
+ );
+}