Files
anchorhost-frontend/src/features/support/TicketSystem.jsx
T

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;