320 lines
18 KiB
JavaScript
320 lines
18 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Head, Link, router } from '@inertiajs/react';
|
|
import AdminLayout from '@/Layouts/AdminLayout';
|
|
import {
|
|
LifeBuoy,
|
|
MessageSquare,
|
|
AlertCircle,
|
|
CheckCircle,
|
|
Clock,
|
|
Search,
|
|
Eye,
|
|
Plus,
|
|
User,
|
|
ShieldAlert,
|
|
TrendingUp,
|
|
Inbox
|
|
} from 'lucide-react';
|
|
|
|
export default function Index({ tickets, filters }) {
|
|
const [searchQuery, setSearchQuery] = useState(filters?.search || '');
|
|
const [statusFilter, setStatusFilter] = useState(filters?.status || 'all');
|
|
const [priorityFilter, setPriorityFilter] = useState(filters?.priority || 'all');
|
|
|
|
const handleSearch = (e) => {
|
|
e.preventDefault();
|
|
applyFilters({
|
|
search: searchQuery,
|
|
status: statusFilter,
|
|
priority: priorityFilter,
|
|
});
|
|
};
|
|
|
|
const applyFilters = (newFilters) => {
|
|
router.get('/admin/tickets', newFilters, {
|
|
preserveState: true,
|
|
replace: true,
|
|
});
|
|
};
|
|
|
|
const handleFilterChange = (key, value) => {
|
|
if (key === 'status') {
|
|
setStatusFilter(value);
|
|
applyFilters({ search: searchQuery, status: value, priority: priorityFilter });
|
|
} else if (key === 'priority') {
|
|
setPriorityFilter(value);
|
|
applyFilters({ search: searchQuery, status: statusFilter, priority: value });
|
|
}
|
|
};
|
|
|
|
// Calculate metrics locally from the tickets list
|
|
const totalTickets = tickets.length;
|
|
const openTickets = tickets.filter(t => t.status === 'open').length;
|
|
const inProgressTickets = tickets.filter(t => t.status === 'in_progress').length;
|
|
const resolvedTickets = tickets.filter(t => t.status === 'resolved' || t.status === 'closed').length;
|
|
|
|
const getStatusBadge = (status) => {
|
|
switch (status) {
|
|
case 'open':
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider bg-blue-50 text-blue-700 border border-blue-200">
|
|
Open
|
|
</span>
|
|
);
|
|
case 'in_progress':
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider bg-amber-50 text-amber-700 border border-amber-200">
|
|
In Progress
|
|
</span>
|
|
);
|
|
case 'resolved':
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider bg-emerald-50 text-emerald-700 border border-emerald-200">
|
|
Resolved
|
|
</span>
|
|
);
|
|
case 'closed':
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider bg-slate-100 text-slate-600 border border-slate-200">
|
|
Closed
|
|
</span>
|
|
);
|
|
default:
|
|
return <span className="text-slate-600">{status}</span>;
|
|
}
|
|
};
|
|
|
|
const getPriorityBadge = (priority) => {
|
|
switch (priority) {
|
|
case 'high':
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase bg-red-50 text-red-700 border border-red-100">
|
|
High
|
|
</span>
|
|
);
|
|
case 'medium':
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase bg-blue-50 text-blue-700 border border-blue-100">
|
|
Medium
|
|
</span>
|
|
);
|
|
case 'low':
|
|
return (
|
|
<span className="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-black uppercase bg-slate-100 text-slate-600 border border-slate-200">
|
|
Low
|
|
</span>
|
|
);
|
|
default:
|
|
return <span className="text-slate-600">{priority}</span>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AdminLayout title="Support Tickets">
|
|
<Head title="Support Ticket Management - Admin Portal" />
|
|
|
|
<div className="font-sans max-w-[1600px] mx-auto space-y-6 select-none">
|
|
|
|
{/* Header Row */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Support Ticket Center</h1>
|
|
<p className="text-sm text-slate-500 mt-1">Review user issues, update priorities, and generate Senior Developer support responses.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upper Metric Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{/* Metric 1 */}
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<span className="text-xs text-slate-500 font-semibold block">Total Tickets</span>
|
|
<span className="text-2xl font-extrabold text-slate-900 block">{totalTickets}</span>
|
|
<span className="text-[10px] text-slate-400 font-medium block">All-time received tickets</span>
|
|
</div>
|
|
<div className="p-3 bg-[#185FA5]/10 text-[#185FA5] rounded-2xl">
|
|
<LifeBuoy className="w-6 h-6 animate-spin-slow" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metric 2 */}
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<span className="text-xs text-slate-500 font-semibold block">New / Open</span>
|
|
<span className="text-2xl font-extrabold text-slate-900 block">{openTickets}</span>
|
|
<span className="text-[10px] text-rose-500 font-bold block">Needs immediate reply</span>
|
|
</div>
|
|
<div className="p-3 bg-red-50 text-red-600 rounded-2xl">
|
|
<Clock className="w-6 h-6 animate-pulse" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metric 3 */}
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<span className="text-xs text-slate-500 font-semibold block">In Progress</span>
|
|
<span className="text-2xl font-extrabold text-slate-900 block">{inProgressTickets}</span>
|
|
<span className="text-[10px] text-amber-500 font-bold block">Active investigation</span>
|
|
</div>
|
|
<div className="p-3 bg-amber-50 text-amber-600 rounded-2xl">
|
|
<MessageSquare className="w-6 h-6" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metric 4 */}
|
|
<div className="bg-white p-5 border border-slate-200 rounded-2xl shadow-sm flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<span className="text-xs text-slate-500 font-semibold block">Resolved / Closed</span>
|
|
<span className="text-2xl font-extrabold text-slate-900 block">{resolvedTickets}</span>
|
|
<span className="text-[10px] text-emerald-600 font-bold block">Archived cleanly</span>
|
|
</div>
|
|
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-2xl">
|
|
<CheckCircle className="w-6 h-6" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Frequently Asked Questions */}
|
|
<div className="bg-white rounded-3xl border border-slate-200 p-8 shadow-sm space-y-6">
|
|
<div className="flex items-center gap-2 pb-2">
|
|
<MessageSquare className="w-5 h-5 text-[#185FA5]" />
|
|
<h3 className="text-base font-bold text-slate-900">Frequently Asked Questions</h3>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* FAQ 1 */}
|
|
<div className="p-6 bg-slate-50/50 hover:bg-slate-100/50 rounded-2xl border border-slate-150 transition-colors">
|
|
<h4 className="text-sm font-bold text-slate-800">How long does a ticket resolution take?</h4>
|
|
<p className="text-xs text-slate-500 font-medium leading-relaxed mt-2">
|
|
For priority high tickets, our engineering support response is typically under 2 hours. General queries resolve within 24 hours.
|
|
</p>
|
|
</div>
|
|
|
|
{/* FAQ 2 */}
|
|
<div className="p-6 bg-slate-50/50 hover:bg-slate-100/50 rounded-2xl border border-slate-150 transition-colors">
|
|
<h4 className="text-sm font-bold text-slate-800">What is Tadbeer contract compliance?</h4>
|
|
<p className="text-xs text-slate-500 font-medium leading-relaxed mt-2">
|
|
All worker hirings conform to MOHRE Tadbeer rules. Contracts are electronically generated and uploaded for review.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters Row */}
|
|
<div className="bg-white border border-slate-200 rounded-2xl shadow-sm overflow-hidden">
|
|
<div className="p-4 border-b border-slate-100 flex flex-col lg:flex-row lg:items-center justify-between gap-4 bg-slate-50/20">
|
|
{/* Search form */}
|
|
<form onSubmit={handleSearch} className="relative w-full lg:w-80">
|
|
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search ticket # or subject..."
|
|
value={searchQuery}
|
|
onChange={e => setSearchQuery(e.target.value)}
|
|
className="bg-white border border-slate-200 pl-10 pr-4 py-2 rounded-xl text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-[#185FA5]/20 w-full shadow-sm"
|
|
/>
|
|
</form>
|
|
|
|
{/* Dropdown Filters */}
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<select
|
|
value={statusFilter}
|
|
onChange={e => handleFilterChange('status', e.target.value)}
|
|
className="bg-white border border-slate-200 text-slate-700 text-xs font-bold px-3 py-2 rounded-xl outline-none cursor-pointer hover:bg-slate-50 transition-colors shadow-sm"
|
|
>
|
|
<option value="all">All Status</option>
|
|
<option value="open">Open</option>
|
|
<option value="in_progress">In Progress</option>
|
|
<option value="resolved">Resolved</option>
|
|
<option value="closed">Closed</option>
|
|
</select>
|
|
|
|
<select
|
|
value={priorityFilter}
|
|
onChange={e => handleFilterChange('priority', e.target.value)}
|
|
className="bg-white border border-slate-200 text-slate-700 text-xs font-bold px-3 py-2 rounded-xl outline-none cursor-pointer hover:bg-slate-50 transition-colors shadow-sm"
|
|
>
|
|
<option value="all">All Priority</option>
|
|
<option value="low">Low</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="high">High</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table listing */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left border-collapse text-xs font-semibold text-slate-600">
|
|
<thead>
|
|
<tr className="border-b border-slate-100 bg-slate-50/50 text-slate-400 font-bold uppercase tracking-wider text-[10px]">
|
|
<th className="px-5 py-4">Ticket Number</th>
|
|
<th className="px-5 py-4">Subject</th>
|
|
<th className="px-5 py-4">Raised By</th>
|
|
<th className="px-5 py-4">Priority</th>
|
|
<th className="px-5 py-4">Status</th>
|
|
<th className="px-5 py-4">Created At</th>
|
|
<th className="px-5 py-4 text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{tickets.map((ticket) => (
|
|
<tr key={ticket.id} className="hover:bg-slate-50/40 transition-colors">
|
|
<td className="px-5 py-4 font-mono font-bold text-[#185FA5]">
|
|
{ticket.ticket_number}
|
|
</td>
|
|
<td className="px-5 py-4 font-semibold text-slate-800 max-w-xs truncate">
|
|
{ticket.subject}
|
|
</td>
|
|
<td className="px-5 py-4">
|
|
<div>
|
|
<span className="font-extrabold text-slate-800 block">
|
|
{ticket.creator_name}
|
|
</span>
|
|
<span className={`text-[9px] font-bold block uppercase tracking-wider ${
|
|
ticket.user_type === 'Employer' ? 'text-[#185FA5]' : 'text-emerald-600'
|
|
}`}>
|
|
{ticket.user_type}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-5 py-4">
|
|
{getPriorityBadge(ticket.priority)}
|
|
</td>
|
|
<td className="px-5 py-4">
|
|
{getStatusBadge(ticket.status)}
|
|
</td>
|
|
<td className="px-5 py-4 text-slate-400">
|
|
{ticket.created_at}
|
|
</td>
|
|
<td className="px-5 py-4 text-right">
|
|
<Link
|
|
href={`/admin/tickets/${ticket.id}`}
|
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-white border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors shadow-sm font-bold uppercase tracking-wider text-[10px]"
|
|
>
|
|
<Eye className="w-3.5 h-3.5 text-slate-400" />
|
|
<span>Manage</span>
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
|
|
{tickets.length === 0 && (
|
|
<tr>
|
|
<td colSpan="7" className="p-12 text-center font-bold text-slate-400 uppercase">
|
|
<div className="flex flex-col items-center justify-center space-y-2">
|
|
<Inbox className="w-8 h-8 text-slate-300" />
|
|
<span>No support tickets found matching criteria</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|