188 lines
6.9 KiB
TypeScript
188 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import api from '@/lib/api';
|
|
import CreateTicketModal from '@/components/create-ticket-modal';
|
|
|
|
interface Ticket {
|
|
id: string;
|
|
tenantName: string;
|
|
subject: string;
|
|
category: string;
|
|
priority: string;
|
|
status: string;
|
|
createdAt: string;
|
|
assignee: { id: string; firstName: string; lastName: string } | null;
|
|
_count: { comments: number };
|
|
}
|
|
|
|
const statusColors: Record<string, string> = {
|
|
open: 'bg-yellow-50 text-yellow-700',
|
|
in_progress: 'bg-blue-50 text-blue-700',
|
|
waiting_tenant: 'bg-orange-50 text-orange-700',
|
|
resolved: 'bg-green-50 text-green-700',
|
|
closed: 'bg-surface-100 text-surface-500',
|
|
};
|
|
|
|
const priorityColors: Record<string, string> = {
|
|
low: 'bg-surface-100 text-surface-600',
|
|
normal: 'bg-blue-50 text-blue-600',
|
|
high: 'bg-orange-50 text-orange-600',
|
|
urgent: 'bg-red-50 text-red-600',
|
|
};
|
|
|
|
interface TenantOption {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
}
|
|
|
|
export default function SupportPage() {
|
|
const [tickets, setTickets] = useState<Ticket[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [status, setStatus] = useState('');
|
|
const [priority, setPriority] = useState('');
|
|
const [search, setSearch] = useState('');
|
|
const [page, setPage] = useState(1);
|
|
const [total, setTotal] = useState(0);
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
|
const limit = 20;
|
|
|
|
useEffect(() => {
|
|
api.get('/tenants?limit=100').then((res) => setTenants(res.data.data.items || [])).catch(() => {});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadTickets();
|
|
}, [status, priority, search, page]);
|
|
|
|
async function loadTickets() {
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
|
if (status) params.set('status', status);
|
|
if (priority) params.set('priority', priority);
|
|
if (search) params.set('search', search);
|
|
|
|
const res = await api.get(`/support/tickets?${params}`);
|
|
setTickets(res.data.data.items);
|
|
setTotal(res.data.data.total);
|
|
} catch (err) {
|
|
console.error('Failed to load tickets:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-surface-900">Support Tickets</h2>
|
|
<p className="text-sm text-surface-500">{total} total tickets</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setShowCreate(true)}
|
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm hover:bg-primary-700"
|
|
>
|
|
Create Ticket
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<input
|
|
type="text"
|
|
placeholder="Search tickets or tenants..."
|
|
value={search}
|
|
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
|
className="px-3 py-2 border border-surface-300 rounded-lg text-sm flex-1 min-w-[200px] max-w-xs"
|
|
/>
|
|
<select
|
|
value={status}
|
|
onChange={(e) => { setStatus(e.target.value); setPage(1); }}
|
|
className="px-3 py-2 border border-surface-300 rounded-lg text-sm"
|
|
>
|
|
<option value="">All Status</option>
|
|
<option value="open">Open</option>
|
|
<option value="in_progress">In Progress</option>
|
|
<option value="waiting_tenant">Waiting Tenant</option>
|
|
<option value="resolved">Resolved</option>
|
|
<option value="closed">Closed</option>
|
|
</select>
|
|
<select
|
|
value={priority}
|
|
onChange={(e) => { setPriority(e.target.value); setPage(1); }}
|
|
className="px-3 py-2 border border-surface-300 rounded-lg text-sm"
|
|
>
|
|
<option value="">All Priority</option>
|
|
<option value="urgent">Urgent</option>
|
|
<option value="high">High</option>
|
|
<option value="normal">Normal</option>
|
|
<option value="low">Low</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl border border-surface-200">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-surface-200 bg-surface-50">
|
|
<th className="text-left px-4 py-3 font-medium text-surface-600">Subject</th>
|
|
<th className="text-left px-4 py-3 font-medium text-surface-600">Tenant</th>
|
|
<th className="text-center px-4 py-3 font-medium text-surface-600">Priority</th>
|
|
<th className="text-center px-4 py-3 font-medium text-surface-600">Status</th>
|
|
<th className="text-left px-4 py-3 font-medium text-surface-600">Assignee</th>
|
|
<th className="text-center px-4 py-3 font-medium text-surface-600">Comments</th>
|
|
<th className="text-left px-4 py-3 font-medium text-surface-600">Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading ? (
|
|
<tr><td colSpan={7} className="text-center py-8 text-surface-400">Loading...</td></tr>
|
|
) : tickets.length === 0 ? (
|
|
<tr><td colSpan={7} className="text-center py-8 text-surface-400">No tickets found</td></tr>
|
|
) : (
|
|
tickets.map((t) => (
|
|
<tr key={t.id} className="border-b border-surface-100 hover:bg-surface-50">
|
|
<td className="px-4 py-3">
|
|
<Link href={`/support/${t.id}`} className="text-primary-600 hover:underline font-medium">
|
|
{t.subject}
|
|
</Link>
|
|
</td>
|
|
<td className="px-4 py-3 text-surface-500">{t.tenantName}</td>
|
|
<td className="px-4 py-3 text-center">
|
|
<span className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${priorityColors[t.priority] || ''}`}>
|
|
{t.priority}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-center">
|
|
<span className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[t.status] || ''}`}>
|
|
{t.status.replace(/_/g, ' ')}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-surface-500">
|
|
{t.assignee ? `${t.assignee.firstName} ${t.assignee.lastName}` : 'Unassigned'}
|
|
</td>
|
|
<td className="px-4 py-3 text-center">{t._count.comments}</td>
|
|
<td className="px-4 py-3 text-surface-500">
|
|
{new Date(t.createdAt).toLocaleDateString()}
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{showCreate && (
|
|
<CreateTicketModal
|
|
tenants={tenants}
|
|
onClose={() => setShowCreate(false)}
|
|
onSuccess={() => loadTickets()}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|