feat: Implement frontend for business owners' support ticket system
This commit is contained in:
201
frontend/src/pages/TicketsPage.tsx
Normal file
201
frontend/src/pages/TicketsPage.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user