feat: Implement frontend for business owners' support ticket system

This commit is contained in:
poduck
2025-11-28 04:56:48 -05:00
parent aa3854a13f
commit 512d95ca2d
10 changed files with 884 additions and 5 deletions

View File

@@ -0,0 +1,201 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Plus, Tag, Clock, CircleDot, User, ArrowRight } from 'lucide-react';
import { useTickets } from '../hooks/useTickets';
import { Ticket } from '../types';
import TicketModal from '../components/TicketModal';
const TicketStatusBadge: React.FC<{ status: Ticket['status'] }> = ({ status }) => {
let colorClass = '';
switch (status) {
case 'OPEN':
colorClass = 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
break;
case 'IN_PROGRESS':
colorClass = 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
break;
case 'RESOLVED':
colorClass = 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
break;
case 'CLOSED':
colorClass = 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
break;
default:
colorClass = 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
{status.replace('_', ' ').toLowerCase()}
</span>
);
};
const TicketPriorityBadge: React.FC<{ priority: Ticket['priority'] }> = ({ priority }) => {
let colorClass = '';
switch (priority) {
case 'LOW':
colorClass = 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
break;
case 'MEDIUM':
colorClass = 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300';
break;
case 'HIGH':
colorClass = 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300';
break;
case 'URGENT':
colorClass = 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
break;
default:
colorClass = 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
}
return (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colorClass}`}>
{priority.toLowerCase()}
</span>
);
};
const TicketsPage: React.FC = () => {
const { t } = useTranslation();
const { data: tickets, isLoading, error } = useTickets({ type: 'PLATFORM' }); // Filter for platform tickets
const [isTicketModalOpen, setIsTicketModalOpen] = useState(false);
const [selectedTicket, setSelectedTicket] = useState<Ticket | null>(null);
const openTicketModal = (ticket: Ticket | null = null) => {
setSelectedTicket(ticket);
setIsTicketModalOpen(true);
};
const closeTicketModal = () => {
setSelectedTicket(null);
setIsTicketModalOpen(false);
};
if (isLoading) {
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-600"></div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-8 max-w-7xl mx-auto">
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p className="text-red-800 dark:text-red-300">{t('tickets.errorLoading')}: {(error as Error).message}</p>
</div>
</div>
);
}
return (
<div className="p-8 max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{t('tickets.title')}</h2>
<p className="text-gray-500 dark:text-gray-400">{t('tickets.description')}</p>
</div>
<button
onClick={() => openTicketModal()}
className="flex items-center justify-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors shadow-sm font-medium"
>
<Plus size={18} />
{t('tickets.newTicket')}
</button>
</div>
{/* Tickets List */}
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden transition-colors duration-200">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('tickets.subject')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('tickets.status')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('tickets.priority')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('tickets.category')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('tickets.assignee')}
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t('tickets.createdAt')}
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">{t('common.actions')}</span>
</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{tickets && tickets.length > 0 ? (
tickets.map((ticket) => (
<tr key={ticket.id} className="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="text-sm font-medium text-gray-900 dark:text-white">{ticket.subject}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<TicketStatusBadge status={ticket.status} />
</td>
<td className="px-6 py-4 whitespace-nowrap">
<TicketPriorityBadge priority={ticket.priority} />
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{ticket.category || t('common.none')}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{ticket.assigneeFullName ? (
<div className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
<User size={16} className="text-brand-500" />
{ticket.assigneeFullName}
</div>
) : (
<span className="text-sm text-gray-500 dark:text-gray-400">{t('tickets.unassigned')}</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{new Date(ticket.createdAt).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onClick={() => openTicketModal(ticket)} className="text-brand-600 hover:text-brand-900 dark:text-brand-400 dark:hover:text-brand-300">
{t('common.view')} <ArrowRight size={16} className="inline-block ml-1" />
</button>
</td>
</tr>
))
) : (
<tr>
<td colSpan={7} className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
{t('tickets.noTicketsFound')}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Ticket Create/Detail Modal */}
{isTicketModalOpen && (
<TicketModal
ticket={selectedTicket}
onClose={closeTicketModal}
/>
)}
</div>
);
};
export default TicketsPage;