Add System Ticket

This commit is contained in:
2026-01-14 21:05:22 +06:00
parent f0a4ad177e
commit cf131bb04e
3 changed files with 513 additions and 0 deletions

View File

@@ -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 <Clock className="w-5 h-5 text-yellow-500" />;
case 'in_progress':
return <AlertCircle className="w-5 h-5 text-blue-500" />;
case 'closed':
return <CheckCircle className="w-5 h-5 text-green-500" />;
default:
return <Clock className="w-5 h-5" />;
}
};
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 (
<div className={`h-full flex flex-col ${theme.primary}`}>
{/* Header */}
<div className={`${theme.secondary} ${theme.border} border-b p-4`}>
<div className="flex items-center gap-4 mb-3">
<button
onClick={onBack}
className={`${theme.hover} p-2 rounded-lg transition`}
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="flex-1">
<h2 className="text-lg font-bold">{currentTicket.title}</h2>
<p className={`text-sm ${theme.textSecondary}`}>
Автор: {currentTicket.author} Создан: {new Date(currentTicket.created_at).toLocaleString('ru-RU')}
</p>
</div>
<div className={`px-3 py-2 rounded-lg border flex items-center gap-2 ${getStatusColor(currentTicket.status)}`}>
{getStatusIcon(currentTicket.status)}
<span className="font-medium">{getStatusText(currentTicket.status)}</span>
</div>
</div>
{/* Status Controls */}
{canChangeStatus && (
<div className="flex gap-2">
<button
onClick={() => changeStatus('pending')}
disabled={currentTicket.status === 'pending'}
className={`flex-1 px-3 py-2 rounded-lg border transition ${
currentTicket.status === 'pending'
? 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50'
: `${theme.card} ${theme.hover} ${theme.border}`
}`}
>
<Clock className="w-4 h-4 inline mr-2" />
На рассмотрении
</button>
<button
onClick={() => changeStatus('in_progress')}
disabled={currentTicket.status === 'in_progress'}
className={`flex-1 px-3 py-2 rounded-lg border transition ${
currentTicket.status === 'in_progress'
? 'bg-blue-500/20 text-blue-500 border-blue-500/50'
: `${theme.card} ${theme.hover} ${theme.border}`
}`}
>
<AlertCircle className="w-4 h-4 inline mr-2" />
В работе
</button>
<button
onClick={() => changeStatus('closed')}
disabled={currentTicket.status === 'closed'}
className={`flex-1 px-3 py-2 rounded-lg border transition ${
currentTicket.status === 'closed'
? 'bg-green-500/20 text-green-500 border-green-500/50'
: `${theme.card} ${theme.hover} ${theme.border}`
}`}
>
<CheckCircle className="w-4 h-4 inline mr-2" />
Закрыт
</button>
</div>
)}
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg, index) => (
<div
key={index}
className={`flex ${msg.author === user.username ? 'justify-end' : 'justify-start'}`}
>
<div
className={`max-w-[70%] rounded-2xl px-4 py-3 ${
msg.author === 'system'
? `${theme.tertiary} ${theme.border} border text-center`
: msg.author === user.username
? `${theme.accent} text-white`
: `${theme.card} ${theme.border} border`
}`}
>
{msg.author !== 'system' && msg.author !== user.username && (
<div className={`text-xs font-semibold mb-1 ${theme.textSecondary}`}>
{msg.author}
</div>
)}
<div className="whitespace-pre-wrap break-words">{msg.text}</div>
<div className={`text-xs mt-1 ${
msg.author === user.username ? 'text-white/70' : theme.textSecondary
}`}>
{new Date(msg.timestamp).toLocaleTimeString('ru-RU')}
</div>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Input */}
{currentTicket.status !== 'closed' && (
<form onSubmit={sendMessage} className={`${theme.border} border-t p-4`}>
<div className="flex gap-2">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Введите сообщение..."
disabled={loading}
className={`flex-1 ${theme.input} ${theme.border} border rounded-xl px-4 py-3 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`}
/>
<button
type="submit"
disabled={loading || !newMessage.trim()}
className={`${theme.accent} ${theme.accentHover} px-6 py-3 rounded-xl flex items-center gap-2 text-white transition disabled:opacity-50`}
>
<Send className="w-4 h-4" />
Отправить
</button>
</div>
</form>
)}
</div>
);
}