180 lines
6.3 KiB
JavaScript
180 lines
6.3 KiB
JavaScript
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 <Clock className="w-4 h-4 text-yellow-500" />;
|
||
case 'in_progress':
|
||
return <AlertCircle className="w-4 h-4 text-blue-500" />;
|
||
case 'closed':
|
||
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||
default:
|
||
return <Clock className="w-4 h-4" />;
|
||
}
|
||
};
|
||
|
||
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 (
|
||
<TicketChat
|
||
ticket={selectedTicket}
|
||
token={token}
|
||
user={user}
|
||
theme={theme}
|
||
onBack={() => {
|
||
setSelectedTicket(null);
|
||
loadTickets();
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={`h-full ${theme.primary} ${theme.text} p-6`}>
|
||
<div className="max-w-6xl mx-auto">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h1 className="text-2xl font-bold mb-2">Тикеты</h1>
|
||
<p className={theme.textSecondary}>Система поддержки</p>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className={`${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl flex items-center gap-2 text-white transition`}
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
Создать тикет
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tickets List */}
|
||
{loading ? (
|
||
<div className="text-center py-12">
|
||
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||
<p className={theme.textSecondary}>Загрузка тикетов...</p>
|
||
</div>
|
||
) : tickets.length === 0 ? (
|
||
<div className={`${theme.card} ${theme.border} border rounded-2xl p-12 text-center`}>
|
||
<MessageSquare className={`w-16 h-16 mx-auto mb-4 ${theme.textSecondary} opacity-50`} />
|
||
<p className="text-lg font-medium mb-2">Нет тикетов</p>
|
||
<p className={`text-sm ${theme.textSecondary} mb-4`}>
|
||
Создайте первый тикет для обращения в поддержку
|
||
</p>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className={`${theme.accent} ${theme.accentHover} px-6 py-2 rounded-xl text-white transition`}
|
||
>
|
||
Создать тикет
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="grid gap-4">
|
||
{tickets.map((ticket) => (
|
||
<div
|
||
key={ticket.id}
|
||
onClick={() => setSelectedTicket(ticket)}
|
||
className={`${theme.card} ${theme.border} border rounded-2xl p-6 cursor-pointer ${theme.hover} transition-all duration-200`}
|
||
>
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-semibold mb-2">{ticket.title}</h3>
|
||
<p className={`text-sm ${theme.textSecondary} line-clamp-2`}>
|
||
{ticket.description}
|
||
</p>
|
||
</div>
|
||
<div className={`px-3 py-1 rounded-lg border flex items-center gap-2 ${getStatusColor(ticket.status)}`}>
|
||
{getStatusIcon(ticket.status)}
|
||
<span className="text-sm font-medium">{getStatusText(ticket.status)}</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<span className={theme.textSecondary}>
|
||
Автор: <span className={theme.text}>{ticket.author}</span>
|
||
</span>
|
||
<span className={theme.textSecondary}>•</span>
|
||
<span className={theme.textSecondary}>
|
||
Сообщений: <span className={theme.text}>{ticket.messages?.length || 0}</span>
|
||
</span>
|
||
<span className={theme.textSecondary}>•</span>
|
||
<span className={theme.textSecondary}>
|
||
{new Date(ticket.created_at).toLocaleString('ru-RU')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{showCreateModal && (
|
||
<CreateTicketModal
|
||
token={token}
|
||
theme={theme}
|
||
onClose={() => setShowCreateModal(false)}
|
||
onCreated={handleTicketCreated}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|