// 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 (
{config.label}
);
};
// 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 (
{config.label}
);
};
// 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 (
onSelect(ticket)}
>
{ticket.subject}
{ticket.description}
#{ticket.id}
{formatDate(ticket.created_at)}
{formatTime(ticket.created_at)}
{isAdmin && (
)}
{ticket.message_count || 0} messages
{ticket.last_reply && (
Last reply: {formatDate(ticket.last_reply)}
)}
{isAdmin && (
<>
{ticket.status === 'open' && (
)}
>
)}
);
};
// 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(
,
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 (
{/* Header */}
{isAdmin && (
onUpdateStatus(currentTicket.id, value)}
placeholder="Change status"
icon={Edit}
/>
)}
{/* Messages */}
{currentTicket.messages && currentTicket.messages.map((msg, index) => (
{msg.is_admin ? 'A' : 'U'}
{msg.is_admin ? 'Support Agent' : (msg.user_username || 'User')}
{formatDateTime(msg.created_at)}
{msg.message}
))}
{/* Message Input */}
{currentTicket.status !== 'closed' && (
)}
);
};
// ==================== 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 (
loadTickets()} aria-label="Refresh tickets">
Refresh
setIsCreateModalOpen(true)}>
New ticket
) : (
loadTickets()} aria-label="Refresh tickets">
Refresh
)
}
/>
{/* Stats Overview */}
{/* Tabs and Filters */}
{/* Tabs */}
{[
{ 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 (
);
})}
{/* Filters */}
{/* Tickets List */}
{isLoading ? (
) : filteredTickets.length === 0 ? (
No tickets found
{activeTab === 'my_tickets' && (
)}
) : (
{filteredTickets.map(ticket => (
))}
)}
{/* Modals */}
setIsCreateModalOpen(false)}
onSubmit={handleCreateTicket}
/>
{selectedTicket && (
{
setIsDetailModalOpen(false);
setSelectedTicket(null);
}}
onSendMessage={handleSendMessage}
onUpdateStatus={handleUpdateStatus}
isAdmin={isAdmin}
/>
)}
);
}
export default TicketSystem;