Add i18n foundation (fa-IR/en-US) and localize landing + auth.
Introduce path-prefixed locale routing under app/[lang] with a middleware that detects locale from cookie/Accept-Language (default fa-IR) and redirects. Add fa-IR (source of truth) and en-US dictionaries, a server getDictionary, a client I18nProvider/useT, locale-aware Link + router helpers, and a language switcher. The root [lang] layout sets html lang/dir and the per-locale font (Peyda for fa, Inter for en). Landing sections and the login/register/auth shell now read all copy from the dictionaries; dashboard localization follows in a later commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import api from '@/lib/api';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import type { Ticket, TicketStatus } from '@/types';
|
||||
import { Wrench, Briefcase, User, CheckCircle } from 'lucide-react';
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
open: 'bg-yellow-100 text-yellow-700',
|
||||
waiting: 'bg-orange-100 text-orange-700',
|
||||
answered: 'bg-green-100 text-green-700',
|
||||
closed: 'bg-gray-100 text-gray-500',
|
||||
};
|
||||
|
||||
const priorityColors: Record<string, string> = {
|
||||
low: 'bg-blue-100 text-blue-700',
|
||||
medium: 'bg-yellow-100 text-yellow-700',
|
||||
high: 'bg-red-100 text-red-700',
|
||||
};
|
||||
|
||||
export default function StaffTicketsPage() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const [statusFilter, setStatusFilter] = useState<TicketStatus | ''>('');
|
||||
|
||||
// Determine which department this staff member handles
|
||||
const department = user?.role === 'sales' ? 'sales' : 'technical';
|
||||
const departmentLabel = department === 'technical' ? 'Technical' : 'Sales';
|
||||
const DeptIcon = department === 'technical' ? Wrench : Briefcase;
|
||||
|
||||
const { data: tickets = [], isLoading } = useQuery<Ticket[]>({
|
||||
queryKey: ['staff-tickets', department, statusFilter],
|
||||
queryFn: () => {
|
||||
const params = statusFilter ? `?status=${statusFilter}` : '';
|
||||
return api.get(`/tickets/staff/${department}${params}`).then((r) => r.data);
|
||||
},
|
||||
});
|
||||
|
||||
const unansweredCount = tickets.filter((t) => t.status === 'open' || t.status === 'waiting').length;
|
||||
const answeredCount = tickets.filter((t) => t.status === 'answered').length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2"><DeptIcon className="w-5 h-5" /> {departmentLabel} Tickets</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{tickets.length} total tickets
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="card p-4">
|
||||
<p className="text-xs text-gray-500 uppercase font-semibold">Unanswered</p>
|
||||
<p className={`text-2xl font-bold mt-1 ${unansweredCount > 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
{unansweredCount}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5">Need response</p>
|
||||
</div>
|
||||
<div className="card p-4">
|
||||
<p className="text-xs text-gray-500 uppercase font-semibold">Answered</p>
|
||||
<p className="text-2xl font-bold text-green-600 mt-1">{answeredCount}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5">Waiting for user</p>
|
||||
</div>
|
||||
<div className="card p-4">
|
||||
<p className="text-xs text-gray-500 uppercase font-semibold">Total Open</p>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-1">
|
||||
{tickets.filter((t) => t.status !== 'closed').length}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5">Excluding closed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['', 'open', 'waiting', 'answered', 'closed'] as const).map((status) => (
|
||||
<button
|
||||
key={status}
|
||||
onClick={() => setStatusFilter(status)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
statusFilter === status
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{status === '' ? 'All' : status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tickets List */}
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-2 border-primary-600 border-t-transparent mx-auto" />
|
||||
</div>
|
||||
) : tickets.length === 0 ? (
|
||||
<div className="card p-12 text-center">
|
||||
<CheckCircle className="w-10 h-10 mx-auto text-gray-300" />
|
||||
<h3 className="mt-3 text-lg font-semibold text-gray-700">No tickets</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{statusFilter ? `No ${statusFilter} tickets` : 'No tickets in this department'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{tickets.map((ticket) => (
|
||||
<Link
|
||||
key={ticket.id}
|
||||
href={`/dashboard/tickets/${ticket.id}`}
|
||||
className="card p-4 block hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-semibold text-gray-900 truncate">{ticket.subject}</h3>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[ticket.status]}`}>
|
||||
{ticket.status}
|
||||
</span>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${priorityColors[ticket.priority]}`}>
|
||||
{ticket.priority}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs text-gray-500">
|
||||
{ticket.user && (
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="w-3 h-3" /> {ticket.user.firstName} {ticket.user.lastName} ({ticket.user.email})
|
||||
</span>
|
||||
)}
|
||||
<span>•</span>
|
||||
<span>{new Date(ticket.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
|
||||
<span>•</span>
|
||||
<span>{ticket.messages?.length || 0} msg{(ticket.messages?.length || 0) !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">→</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user