908 lines
34 KiB
React
908 lines
34 KiB
React
// TicketSystem.jsx - Updated with auto-reloading feature
|
|
import React, { useState, useEffect, useContext, useRef, useCallback } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { AuthContext } from '../../App.jsx';
|
|
import useWebSocket from '../../hooks/useWebSocket.js';
|
|
import {
|
|
MessageCircle, Plus, Search, Filter, Clock, AlertCircle, CheckCircle,
|
|
X, User, FileText, Tag, AlertTriangle as Priority,
|
|
Send, Edit, RefreshCw,
|
|
Calendar, Clock as ClockIcon, Users
|
|
} from "lucide-react";
|
|
import { colors } from '../../theme/colors';
|
|
import { dashLayout } from '../../theme/dashboardTokens';
|
|
import { DashboardViewShell, PageHeaderBar, StatTile, ActionButton, Dropdown, SectionCard } from '../../components/dashboardPrimitives';
|
|
import { UnifiedTooltip } from '../../components/DashboardComponents';
|
|
import { WS_BASE_URL } from '../../config/api';
|
|
|
|
// Ticket Status Badge
|
|
const StatusBadge = ({ status }) => {
|
|
const statusConfig = {
|
|
'open': { label: 'Open', color: colors.green, bgColor: `${colors.green}20` },
|
|
'pending': { label: 'Pending', color: colors.yellow, bgColor: `${colors.yellow}20` },
|
|
'in_progress': { label: 'In Progress', color: colors.blue, bgColor: `${colors.blue}20` },
|
|
'resolved': { label: 'Resolved', color: colors.cyan, bgColor: `${colors.cyan}20` },
|
|
'closed': { label: 'Closed', color: colors.magenta, bgColor: `${colors.magenta}20` }
|
|
};
|
|
|
|
const config = statusConfig[status] || statusConfig.open;
|
|
|
|
return (
|
|
<span className="px-3 py-1 rounded-full text-xs font-medium" style={{
|
|
backgroundColor: config.bgColor,
|
|
color: config.color
|
|
}}>
|
|
{config.label}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
// Priority Badge
|
|
const PriorityBadge = ({ priority }) => {
|
|
const priorityConfig = {
|
|
'low': { label: 'Low', color: colors.cyan, bgColor: `${colors.cyan}20` },
|
|
'medium': { label: 'Medium', color: colors.blue, bgColor: `${colors.blue}20` },
|
|
'high': { label: 'High', color: colors.yellow, bgColor: `${colors.yellow}20` },
|
|
'urgent': { label: 'Urgent', color: colors.red, bgColor: `${colors.red}20` },
|
|
'critical': { label: 'Critical', color: colors.brightRed, bgColor: `${colors.brightRed}20` }
|
|
};
|
|
|
|
const config = priorityConfig[priority] || priorityConfig.medium;
|
|
|
|
return (
|
|
<span className="px-3 py-1 rounded-full text-xs font-medium" style={{
|
|
backgroundColor: config.bgColor,
|
|
color: config.color
|
|
}}>
|
|
{config.label}
|
|
</span>
|
|
);
|
|
};
|
|
|
|
// Ticket Card Component
|
|
const TicketCard = ({ ticket, onSelect, onAction, isAdmin = false }) => {
|
|
const formatDate = (dateString) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
});
|
|
};
|
|
|
|
const formatTime = (dateString) => {
|
|
return new Date(dateString).toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="group rounded-2xl border transition-all duration-300 hover:scale-105 hover:shadow-xl cursor-pointer overflow-hidden"
|
|
style={{ backgroundColor: colors.backgroundSecondary, borderColor: colors.brightBlack }}
|
|
onClick={() => onSelect(ticket)}
|
|
>
|
|
<div className="p-5">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex-1">
|
|
<h4 className="font-semibold text-lg mb-2" style={{ color: colors.brightWhite }}>
|
|
{ticket.subject}
|
|
</h4>
|
|
<p className="text-sm line-clamp-2" style={{ color: colors.foreground }}>
|
|
{ticket.description}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center space-x-2 ml-4">
|
|
<StatusBadge status={ticket.status} />
|
|
<PriorityBadge priority={ticket.priority} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
<div className="flex items-center space-x-4" style={{ color: colors.foreground }}>
|
|
<div className="flex items-center space-x-1">
|
|
<Tag className="w-3 h-3" />
|
|
<span>#{ticket.id}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-1">
|
|
<Calendar className="w-3 h-3" />
|
|
<span>{formatDate(ticket.created_at)}</span>
|
|
</div>
|
|
<div className="flex items-center space-x-1">
|
|
<ClockIcon className="w-3 h-3" />
|
|
<span>{formatTime(ticket.created_at)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{isAdmin && (
|
|
<div className="flex items-center space-x-2">
|
|
<div className="flex items-center space-x-1 px-2 py-1 rounded-lg" style={{ backgroundColor: colors.brightBlack }}>
|
|
<User className="w-3 h-3" style={{ color: colors.blue }} />
|
|
<span className="text-xs" style={{ color: colors.foreground }}>{ticket.user_username}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between mt-4 pt-4 border-t" style={{ borderColor: colors.brightBlack }}>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="flex items-center space-x-1 text-xs" style={{ color: colors.foreground }}>
|
|
<MessageCircle className="w-3 h-3" />
|
|
<span>{ticket.message_count || 0} messages</span>
|
|
</div>
|
|
{ticket.last_reply && (
|
|
<div className="flex items-center space-x-1 text-xs" style={{ color: colors.foreground }}>
|
|
<Clock className="w-3 h-3" />
|
|
<span>Last reply: {formatDate(ticket.last_reply)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
{isAdmin && (
|
|
<>
|
|
{ticket.status === 'open' && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onAction('claim', ticket.id);
|
|
}}
|
|
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
|
|
style={{
|
|
backgroundColor: `${colors.blue}20`,
|
|
borderColor: colors.blue,
|
|
color: colors.brightBlue
|
|
}}
|
|
>
|
|
<UnifiedTooltip content="Claim ticket" multiline={false}>
|
|
<User className="w-4 h-4" />
|
|
</UnifiedTooltip>
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onAction('close', ticket.id);
|
|
}}
|
|
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border"
|
|
style={{
|
|
backgroundColor: `${colors.green}20`,
|
|
borderColor: colors.green,
|
|
color: colors.brightGreen
|
|
}}
|
|
>
|
|
<UnifiedTooltip content="Close ticket" multiline={false}>
|
|
<CheckCircle className="w-4 h-4" />
|
|
</UnifiedTooltip>
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Create Ticket Modal
|
|
const CreateTicketModal = ({ isOpen, onClose, onSubmit }) => {
|
|
const [formData, setFormData] = useState({
|
|
subject: '',
|
|
description: '',
|
|
priority: 'medium',
|
|
category: 'general'
|
|
});
|
|
|
|
const categories = [
|
|
{ value: 'general', label: 'General Support' },
|
|
{ value: 'billing', label: 'Billing Issue' },
|
|
{ value: 'technical', label: 'Technical Problem' },
|
|
{ value: 'server', label: 'Server Issue' },
|
|
{ value: 'feature', label: 'Feature Request' },
|
|
{ value: 'other', label: 'Other' }
|
|
];
|
|
|
|
const priorities = [
|
|
{ value: 'low', label: 'Low Priority' },
|
|
{ value: 'medium', label: 'Medium Priority' },
|
|
{ value: 'high', label: 'High Priority' },
|
|
{ value: 'urgent', label: 'Urgent' },
|
|
{ value: 'critical', label: 'Critical' }
|
|
];
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
if (formData.subject.trim() && formData.description.trim()) {
|
|
onSubmit(formData);
|
|
setFormData({ subject: '', description: '', priority: 'medium', category: 'general' });
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-[9999] p-4">
|
|
<div className="rounded-2xl w-full max-w-2xl border" style={{ backgroundColor: colors.backgroundSecondary, borderColor: colors.brightBlack }}>
|
|
<div className="p-6 border-b flex justify-between items-center" style={{ borderColor: colors.brightBlack }}>
|
|
<h3 className="text-xl font-bold" style={{ color: colors.brightWhite }}>Create New Ticket</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 rounded-lg transition-all duration-200 hover:scale-105 border group"
|
|
style={{
|
|
backgroundColor: colors.brightBlack,
|
|
borderColor: colors.brightBlack,
|
|
color: colors.foreground
|
|
}}
|
|
>
|
|
<X className="w-5 h-5 group-hover:rotate-90 transition-transform duration-300" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2" style={{ color: colors.foreground }}>Category</label>
|
|
<Dropdown
|
|
value={formData.category}
|
|
options={categories}
|
|
onChange={(value) => setFormData(prev => ({ ...prev, category: value }))}
|
|
placeholder="Select category"
|
|
icon={Tag}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2" style={{ color: colors.foreground }}>Priority</label>
|
|
<Dropdown
|
|
value={formData.priority}
|
|
options={priorities}
|
|
onChange={(value) => setFormData(prev => ({ ...prev, priority: value }))}
|
|
placeholder="Select priority"
|
|
icon={Priority}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2" style={{ color: colors.foreground }}>Subject</label>
|
|
<input
|
|
type="text"
|
|
value={formData.subject}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, subject: e.target.value }))}
|
|
className="w-full rounded-xl px-4 py-3 text-sm focus:outline-none transition-all duration-200 border"
|
|
style={{
|
|
backgroundColor: colors.brightBlack,
|
|
borderColor: colors.brightBlack,
|
|
color: colors.brightWhite
|
|
}}
|
|
placeholder="Brief description of your issue"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2" style={{ color: colors.foreground }}>Description</label>
|
|
<textarea
|
|
value={formData.description}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
|
rows={6}
|
|
className="w-full rounded-xl px-4 py-3 text-sm focus:outline-none resize-none transition-all duration-200 border"
|
|
style={{
|
|
backgroundColor: colors.brightBlack,
|
|
borderColor: colors.brightBlack,
|
|
color: colors.brightWhite
|
|
}}
|
|
placeholder="Please provide detailed information about your issue..."
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex space-x-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
|
|
style={{
|
|
backgroundColor: colors.brightBlack,
|
|
borderColor: colors.brightBlack,
|
|
color: colors.brightWhite
|
|
}}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="flex-1 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border"
|
|
style={{
|
|
backgroundColor: colors.blue,
|
|
borderColor: colors.blue,
|
|
color: colors.brightWhite
|
|
}}
|
|
>
|
|
Create Ticket
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
);
|
|
};
|
|
|
|
// Ticket Detail Modal
|
|
const TicketDetailModal = ({ ticket, isOpen, onClose, onSendMessage, onUpdateStatus, isAdmin = false }) => {
|
|
const [message, setMessage] = useState('');
|
|
const [currentTicket, setCurrentTicket] = useState(ticket);
|
|
|
|
useEffect(() => {
|
|
setCurrentTicket(ticket);
|
|
}, [ticket]);
|
|
|
|
const handleSendMessage = (e) => {
|
|
e.preventDefault();
|
|
if (message.trim()) {
|
|
onSendMessage(currentTicket.id, message);
|
|
setMessage('');
|
|
}
|
|
};
|
|
|
|
const formatDateTime = (dateString) => {
|
|
return new Date(dateString).toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 p-4">
|
|
<div className="rounded-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col border" style={{ backgroundColor: colors.backgroundSecondary, borderColor: colors.brightBlack }}>
|
|
{/* Header */}
|
|
<div className="p-6 border-b flex justify-between items-center" style={{ borderColor: colors.brightBlack }}>
|
|
<div className="flex items-center space-x-4">
|
|
<div className="p-2 rounded-lg" style={{ backgroundColor: `${colors.blue}20` }}>
|
|
<MessageCircle className="w-6 h-6" style={{ color: colors.blue }} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xl font-bold" style={{ color: colors.brightWhite }}>{currentTicket.subject}</h3>
|
|
<div className="flex items-center space-x-3 mt-1">
|
|
<StatusBadge status={currentTicket.status} />
|
|
<PriorityBadge priority={currentTicket.priority} />
|
|
<span className="text-sm" style={{ color: colors.foreground }}>#{currentTicket.id}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
{isAdmin && (
|
|
<Dropdown
|
|
value={currentTicket.status}
|
|
options={[
|
|
{ value: 'open', label: 'Open' },
|
|
{ value: 'pending', label: 'Pending' },
|
|
{ value: 'in_progress', label: 'In Progress' },
|
|
{ value: 'resolved', label: 'Resolved' },
|
|
{ value: 'closed', label: 'Closed' }
|
|
]}
|
|
onChange={(value) => onUpdateStatus(currentTicket.id, value)}
|
|
placeholder="Change status"
|
|
icon={Edit}
|
|
/>
|
|
)}
|
|
<button
|
|
onClick={onClose}
|
|
className="p-3 rounded-xl transition-all duration-200 hover:scale-105 border group"
|
|
style={{
|
|
backgroundColor: colors.brightBlack,
|
|
borderColor: colors.brightBlack,
|
|
color: colors.foreground
|
|
}}
|
|
>
|
|
<X className="w-5 h-5 group-hover:rotate-90 transition-transform duration-300" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
|
{currentTicket.messages && currentTicket.messages.map((msg, index) => (
|
|
<div key={index} className={`flex ${msg.is_admin ? 'justify-end' : 'justify-start'}`}>
|
|
<div className={`max-w-2xl rounded-2xl p-4 ${msg.is_admin ? 'rounded-br-none' : 'rounded-bl-none'}`}
|
|
style={{
|
|
backgroundColor: msg.is_admin ? `${colors.blue}20` : colors.brightBlack,
|
|
border: `1px solid ${msg.is_admin ? colors.blue : colors.brightBlack}`
|
|
}}>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium"
|
|
style={{
|
|
backgroundColor: msg.is_admin ? colors.blue : colors.magenta,
|
|
color: colors.brightWhite
|
|
}}>
|
|
{msg.is_admin ? 'A' : 'U'}
|
|
</div>
|
|
<span className="text-sm font-medium" style={{ color: colors.brightWhite }}>
|
|
{msg.is_admin ? 'Support Agent' : (msg.user_username || 'User')}
|
|
</span>
|
|
</div>
|
|
<span className="text-xs" style={{ color: colors.foreground }}>
|
|
{formatDateTime(msg.created_at)}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm" style={{ color: colors.brightWhite }}>
|
|
{msg.message}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Message Input */}
|
|
{currentTicket.status !== 'closed' && (
|
|
<div className="p-6 border-t" style={{ borderColor: colors.brightBlack }}>
|
|
<form onSubmit={handleSendMessage} className="flex space-x-3">
|
|
<input
|
|
type="text"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
className="flex-1 rounded-xl px-4 py-3 text-sm focus:outline-none transition-all duration-200 border"
|
|
style={{
|
|
backgroundColor: colors.brightBlack,
|
|
borderColor: colors.brightBlack,
|
|
color: colors.brightWhite
|
|
}}
|
|
placeholder="Type your message..."
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:scale-105 border flex items-center space-x-2"
|
|
style={{
|
|
backgroundColor: colors.blue,
|
|
borderColor: colors.blue,
|
|
color: colors.brightWhite
|
|
}}
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
<span>Send</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// ==================== MAIN TICKET SYSTEM COMPONENT ====================
|
|
function TicketSystem() {
|
|
const { user, isAuthenticated } = useContext(AuthContext);
|
|
const [activeTab, setActiveTab] = useState('my_tickets');
|
|
const [tickets, setTickets] = useState([]);
|
|
const [filteredTickets, setFilteredTickets] = useState([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
const [priorityFilter, setPriorityFilter] = useState('all');
|
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
const [selectedTicket, setSelectedTicket] = useState(null);
|
|
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
|
|
|
|
const isAdmin = user?.role === 'admin';
|
|
const token = localStorage.getItem('anchorhost_token');
|
|
const WS_URL = WS_BASE_URL;
|
|
|
|
// Ref to store sendMessage and state for refreshing tickets
|
|
const refreshTicketsRef = useRef({ sendMessage: null, activeTab, priorityFilter });
|
|
|
|
// WebSocket connection for real-time updates
|
|
const handleWebSocketMessage = useCallback((data) => {
|
|
console.log('TicketSystem received message:', data.type, data);
|
|
|
|
// Helper function to refresh tickets list
|
|
const refreshTickets = () => {
|
|
const { sendMessage: sm, activeTab: at, priorityFilter: pf } = refreshTicketsRef.current;
|
|
if (sm) {
|
|
setIsLoading(true);
|
|
sm({
|
|
type: 'get_tickets',
|
|
status_filter: at === 'closed_tickets' ? 'closed' :
|
|
at === 'open_tickets' ? 'open' : undefined,
|
|
priority_filter: pf !== 'all' ? pf : undefined
|
|
});
|
|
}
|
|
};
|
|
|
|
switch (data.type) {
|
|
case 'tickets_list':
|
|
setTickets(data.tickets || []);
|
|
setIsLoading(false);
|
|
break;
|
|
case 'ticket_created':
|
|
// Refresh tickets list immediately after creation
|
|
refreshTickets();
|
|
break;
|
|
case 'ticket_details':
|
|
setSelectedTicket(data.ticket);
|
|
break;
|
|
case 'ticket_message_sent':
|
|
// Update the ticket with new message
|
|
setTickets(prev => prev.map(ticket => {
|
|
if (ticket.id === data.message.ticket_id) {
|
|
return {
|
|
...ticket,
|
|
messages: [...(ticket.messages || []), data.message],
|
|
message_count: (ticket.message_count || 0) + 1,
|
|
last_reply: new Date().toISOString()
|
|
};
|
|
}
|
|
return ticket;
|
|
}));
|
|
|
|
// Update selected ticket if open
|
|
if (selectedTicket && selectedTicket.id === data.message.ticket_id) {
|
|
setSelectedTicket(prev => ({
|
|
...prev,
|
|
messages: [...(prev.messages || []), data.message],
|
|
message_count: (prev.message_count || 0) + 1,
|
|
last_reply: new Date().toISOString()
|
|
}));
|
|
}
|
|
break;
|
|
case 'ticket_status_updated':
|
|
// Refresh tickets list after status update
|
|
refreshTickets();
|
|
break;
|
|
case 'ticket_stats':
|
|
// Handle stats update if needed
|
|
break;
|
|
case 'admin_notification_new_ticket':
|
|
if (isAdmin) {
|
|
// Refresh tickets list for admin
|
|
refreshTickets();
|
|
}
|
|
break;
|
|
case 'ticket_new_message':
|
|
// Refresh tickets list for real-time updates
|
|
refreshTickets();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}, [isAdmin, selectedTicket]);
|
|
|
|
const { sendMessage } = useWebSocket(
|
|
`${WS_URL}?token=${token}`,
|
|
handleWebSocketMessage
|
|
);
|
|
|
|
// ==================== AUTO-RELOADING IMPLEMENTATION ====================
|
|
|
|
// Update ref when sendMessage or filters change
|
|
useEffect(() => {
|
|
refreshTicketsRef.current = { sendMessage, activeTab, priorityFilter };
|
|
}, [sendMessage, activeTab, priorityFilter]);
|
|
|
|
// Load tickets function
|
|
const loadTickets = useCallback(() => {
|
|
setIsLoading(true);
|
|
sendMessage({
|
|
type: 'get_tickets',
|
|
status_filter: activeTab === 'closed_tickets' ? 'closed' :
|
|
activeTab === 'open_tickets' ? 'open' : undefined,
|
|
priority_filter: priorityFilter !== 'all' ? priorityFilter : undefined
|
|
});
|
|
}, [activeTab, priorityFilter, sendMessage]);
|
|
|
|
// Auto-refresh tickets every 30 seconds
|
|
useEffect(() => {
|
|
if (isAuthenticated && sendMessage) {
|
|
loadTickets();
|
|
const interval = setInterval(loadTickets, 30000);
|
|
return () => clearInterval(interval);
|
|
}
|
|
}, [isAuthenticated, loadTickets, sendMessage]);
|
|
|
|
// ==================== END AUTO-RELOADING IMPLEMENTATION ====================
|
|
|
|
const filterTickets = useCallback(() => {
|
|
let filtered = tickets;
|
|
|
|
// Filter by search query
|
|
if (searchQuery) {
|
|
filtered = filtered.filter(ticket =>
|
|
ticket.subject.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
ticket.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
ticket.id.toLowerCase().includes(searchQuery.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// Filter by status
|
|
if (statusFilter !== 'all') {
|
|
filtered = filtered.filter(ticket => ticket.status === statusFilter);
|
|
}
|
|
|
|
// Filter by priority
|
|
if (priorityFilter !== 'all') {
|
|
filtered = filtered.filter(ticket => ticket.priority === priorityFilter);
|
|
}
|
|
|
|
setFilteredTickets(filtered);
|
|
}, [tickets, searchQuery, statusFilter, priorityFilter]);
|
|
|
|
// Filter tickets based on search and filters
|
|
useEffect(() => {
|
|
filterTickets();
|
|
}, [filterTickets]);
|
|
|
|
const handleCreateTicket = (ticketData) => {
|
|
sendMessage({
|
|
type: 'create_ticket',
|
|
subject: ticketData.subject,
|
|
description: ticketData.description,
|
|
priority: ticketData.priority,
|
|
category: ticketData.category
|
|
});
|
|
setIsCreateModalOpen(false);
|
|
};
|
|
|
|
const handleSendMessage = (ticketId, message) => {
|
|
sendMessage({
|
|
type: 'send_ticket_message',
|
|
ticket_id: ticketId,
|
|
message: message
|
|
});
|
|
};
|
|
|
|
const handleUpdateStatus = (ticketId, newStatus) => {
|
|
sendMessage({
|
|
type: 'update_ticket_status',
|
|
ticket_id: ticketId,
|
|
status: newStatus
|
|
});
|
|
};
|
|
|
|
const handleTicketAction = (action, ticketId) => {
|
|
switch (action) {
|
|
case 'claim':
|
|
// Implement claim logic
|
|
break;
|
|
case 'close':
|
|
handleUpdateStatus(ticketId, 'closed');
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
const handleSelectTicket = (ticket) => {
|
|
setSelectedTicket(ticket);
|
|
setIsDetailModalOpen(true);
|
|
|
|
// Load full ticket details
|
|
sendMessage({
|
|
type: 'get_ticket',
|
|
ticket_id: ticket.id
|
|
});
|
|
};
|
|
|
|
const stats = {
|
|
total: tickets.length,
|
|
open: tickets.filter(t => t.status === 'open').length,
|
|
pending: tickets.filter(t => t.status === 'pending').length,
|
|
in_progress: tickets.filter(t => t.status === 'in_progress').length,
|
|
resolved: tickets.filter(t => t.status === 'resolved').length,
|
|
closed: tickets.filter(t => t.status === 'closed').length
|
|
};
|
|
|
|
return (
|
|
<div className="w-full space-y-6">
|
|
<DashboardViewShell>
|
|
<PageHeaderBar
|
|
accentColor={colors.magenta}
|
|
icon={MessageCircle}
|
|
title={isAdmin ? 'Support queue' : 'Support'}
|
|
description={
|
|
isAdmin ? 'Manage customer tickets' : 'Get help with your servers and account'
|
|
}
|
|
actions={
|
|
!isAdmin ? (
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<ActionButton variant="secondary" onClick={() => loadTickets()} aria-label="Refresh tickets">
|
|
<RefreshCw className="w-4 h-4 shrink-0" aria-hidden />
|
|
<span>Refresh</span>
|
|
</ActionButton>
|
|
<ActionButton variant="primary" onClick={() => setIsCreateModalOpen(true)}>
|
|
<Plus className="w-4 h-4 shrink-0" aria-hidden />
|
|
<span>New ticket</span>
|
|
</ActionButton>
|
|
</div>
|
|
) : (
|
|
<ActionButton variant="secondary" onClick={() => loadTickets()} aria-label="Refresh tickets">
|
|
<RefreshCw className="w-4 h-4 shrink-0" aria-hidden />
|
|
<span>Refresh</span>
|
|
</ActionButton>
|
|
)
|
|
}
|
|
/>
|
|
|
|
<div className={dashLayout.bodyPadding}>
|
|
{/* Stats Overview */}
|
|
<SectionCard title="Ticket overview" icon={MessageCircle} color={colors.blue}>
|
|
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
|
<StatTile label="Total" value={stats.total} accentColor={colors.blue} />
|
|
<StatTile label="Open" value={stats.open} accentColor={colors.green} />
|
|
<StatTile label="Pending" value={stats.pending} accentColor={colors.yellow} />
|
|
<StatTile label="In progress" value={stats.in_progress} accentColor={colors.blue} />
|
|
<StatTile label="Resolved" value={stats.resolved} accentColor={colors.cyan} />
|
|
<StatTile label="Closed" value={stats.closed} accentColor={colors.magenta} />
|
|
</div>
|
|
</SectionCard>
|
|
|
|
{/* Tabs and Filters */}
|
|
<SectionCard title="Ticket Management" icon={Filter} color={colors.magenta}>
|
|
<div className="space-y-6">
|
|
{/* Tabs */}
|
|
<div className="border-b border-white/5" style={{ borderColor: colors.brightBlack }}>
|
|
<div className="flex flex-wrap gap-1">
|
|
{[
|
|
{ id: 'my_tickets', label: 'My Tickets', icon: User },
|
|
...(isAdmin ? [
|
|
{ id: 'open_tickets', label: 'Open Tickets', icon: AlertCircle },
|
|
{ id: 'all_tickets', label: 'All Tickets', icon: Users }
|
|
] : []),
|
|
{ id: 'closed_tickets', label: 'Closed Tickets', icon: CheckCircle }
|
|
].map((tab) => {
|
|
const Icon = tab.icon;
|
|
const isActive = activeTab === tab.id;
|
|
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
type="button"
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className="flex items-center space-x-2 px-4 py-3 text-sm font-medium rounded-t-lg border-b-2 transition-all duration-200"
|
|
style={{
|
|
color: isActive ? colors.blue : colors.foreground,
|
|
borderBottomColor: isActive ? colors.blue : 'transparent',
|
|
backgroundColor: isActive ? `${colors.blue}08` : 'transparent',
|
|
opacity: isActive ? 1 : 0.75,
|
|
}}
|
|
>
|
|
<Icon className="w-4 h-4 shrink-0" />
|
|
<span>{tab.label}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
<div className="flex-1">
|
|
<div className="relative">
|
|
<Search className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2" style={{ color: colors.foreground }} />
|
|
<input
|
|
type="text"
|
|
placeholder="Search tickets..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-3 rounded-xl text-sm focus:outline-none transition-all duration-200 border border-white/10 focus:bg-black/40"
|
|
style={{
|
|
backgroundColor: colors.black,
|
|
borderColor: colors.brightBlack,
|
|
color: colors.brightWhite,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
<div className="w-32">
|
|
<Dropdown
|
|
value={statusFilter}
|
|
options={[
|
|
{ value: 'all', label: 'All Status' },
|
|
{ value: 'open', label: 'Open' },
|
|
{ value: 'pending', label: 'Pending' },
|
|
{ value: 'in_progress', label: 'In Progress' },
|
|
{ value: 'resolved', label: 'Resolved' },
|
|
{ value: 'closed', label: 'Closed' }
|
|
]}
|
|
onChange={setStatusFilter}
|
|
placeholder="Status"
|
|
icon={Filter}
|
|
/>
|
|
</div>
|
|
|
|
<div className="w-32">
|
|
<Dropdown
|
|
value={priorityFilter}
|
|
options={[
|
|
{ value: 'all', label: 'All Priority' },
|
|
{ value: 'low', label: 'Low' },
|
|
{ value: 'medium', label: 'Medium' },
|
|
{ value: 'high', label: 'High' },
|
|
{ value: 'urgent', label: 'Urgent' },
|
|
{ value: 'critical', label: 'Critical' }
|
|
]}
|
|
onChange={setPriorityFilter}
|
|
placeholder="Priority"
|
|
icon={Priority}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SectionCard>
|
|
|
|
{/* Tickets List */}
|
|
<SectionCard title={`${filteredTickets.length} Tickets Found`} icon={FileText} color={colors.cyan}>
|
|
{isLoading ? (
|
|
<div className="flex justify-center items-center h-32">
|
|
<RefreshCw className="w-8 h-8 animate-spin" style={{ color: colors.blue }} />
|
|
</div>
|
|
) : filteredTickets.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<MessageCircle className="w-16 h-16 mx-auto mb-4" style={{ color: colors.foreground, opacity: 0.5 }} />
|
|
<p className="text-lg mb-4" style={{ color: colors.foreground }}>No tickets found</p>
|
|
{activeTab === 'my_tickets' && (
|
|
<button
|
|
onClick={() => setIsCreateModalOpen(true)}
|
|
className="px-8 py-3 rounded-xl transition-all duration-200 hover:scale-105 border"
|
|
style={{
|
|
backgroundColor: colors.blue,
|
|
borderColor: colors.blue,
|
|
color: colors.brightWhite
|
|
}}
|
|
>
|
|
Create Your First Ticket
|
|
</button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{filteredTickets.map(ticket => (
|
|
<TicketCard
|
|
key={ticket.id}
|
|
ticket={ticket}
|
|
onSelect={handleSelectTicket}
|
|
onAction={handleTicketAction}
|
|
isAdmin={isAdmin}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</SectionCard>
|
|
</div>
|
|
</DashboardViewShell>
|
|
|
|
{/* Modals */}
|
|
<CreateTicketModal
|
|
isOpen={isCreateModalOpen}
|
|
onClose={() => setIsCreateModalOpen(false)}
|
|
onSubmit={handleCreateTicket}
|
|
/>
|
|
|
|
{selectedTicket && (
|
|
<TicketDetailModal
|
|
ticket={selectedTicket}
|
|
isOpen={isDetailModalOpen}
|
|
onClose={() => {
|
|
setIsDetailModalOpen(false);
|
|
setSelectedTicket(null);
|
|
}}
|
|
onSendMessage={handleSendMessage}
|
|
onUpdateStatus={handleUpdateStatus}
|
|
isAdmin={isAdmin}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default TicketSystem;
|