From 1b9bd81687ccbf7b44f50f3d38ae07cacca03626 Mon Sep 17 00:00:00 2001 From: Yamamoto Date: Mon, 5 Jan 2026 11:26:45 -0300 Subject: [PATCH] feat(tickets): add pagination, skeleton loading, create modal, i18n support --- frontend/src/app/dashboard/tickets/page.tsx | 229 +++++++++++++++++--- frontend/src/i18n/en.json | 60 ++++- frontend/src/i18n/es.json | 60 ++++- frontend/src/i18n/pt-BR.json | 60 ++++- 4 files changed, 371 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/dashboard/tickets/page.tsx b/frontend/src/app/dashboard/tickets/page.tsx index 836da87..75562e9 100644 --- a/frontend/src/app/dashboard/tickets/page.tsx +++ b/frontend/src/app/dashboard/tickets/page.tsx @@ -15,7 +15,7 @@ import { Badge } from "@/components/ui/badge" import { Ticket, ticketsApi } from "@/lib/api" import { toast } from "sonner" import Link from "next/link" -import { MessageSquare, Trash2, Eye } from "lucide-react" +import { Plus, Trash2, Eye, ChevronLeft, ChevronRight } from "lucide-react" import { format } from "date-fns" import { Dialog, @@ -25,11 +25,31 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Skeleton } from "@/components/ui/skeleton" +import { useTranslation } from "@/lib/i18n" + +const ITEMS_PER_PAGE = 10 export default function AdminTicketsPage() { + const { t } = useTranslation() const [tickets, setTickets] = useState([]) const [loading, setLoading] = useState(true) const [deleteId, setDeleteId] = useState(null) + const [createOpen, setCreateOpen] = useState(false) + const [creating, setCreating] = useState(false) + const [newTicket, setNewTicket] = useState({ subject: "", message: "", priority: "medium" }) + + // Pagination state + const [currentPage, setCurrentPage] = useState(1) + const totalPages = Math.ceil(tickets.length / ITEMS_PER_PAGE) + const paginatedTickets = tickets.slice( + (currentPage - 1) * ITEMS_PER_PAGE, + currentPage * ITEMS_PER_PAGE + ) const fetchTickets = async () => { try { @@ -38,7 +58,7 @@ export default function AdminTicketsPage() { setTickets(data) } catch (error) { console.error("Failed to fetch tickets", error) - toast.error("Failed to fetch tickets") + toast.error(t("ticketsPage.errors.fetchFailed")) } finally { setLoading(false) } @@ -52,91 +72,150 @@ export default function AdminTicketsPage() { if (!deleteId) return try { await ticketsApi.delete(deleteId) - toast.success("Ticket deleted") + toast.success(t("ticketsPage.messages.deleted")) setDeleteId(null) fetchTickets() } catch (error) { console.error("Failed to delete ticket", error) - toast.error("Failed to delete ticket") + toast.error(t("ticketsPage.errors.deleteFailed")) + } + } + + const handleCreate = async () => { + if (!newTicket.subject.trim()) { + toast.error(t("ticketsPage.errors.subjectRequired")) + return + } + try { + setCreating(true) + await ticketsApi.create(newTicket.subject, newTicket.priority) + toast.success(t("ticketsPage.messages.created")) + setCreateOpen(false) + setNewTicket({ subject: "", message: "", priority: "medium" }) + fetchTickets() + } catch (error) { + console.error("Failed to create ticket", error) + toast.error(t("ticketsPage.errors.createFailed")) + } finally { + setCreating(false) } } const getStatusBadge = (status: string) => { + const labels: Record = { + open: t("ticketsPage.status.open"), + in_progress: t("ticketsPage.status.inProgress"), + closed: t("ticketsPage.status.closed"), + } switch (status) { - case "open": return Open - case "in_progress": return In Progress - case "closed": return Closed + case "open": return {labels.open} + case "in_progress": return {labels.in_progress} + case "closed": return {labels.closed} default: return {status} } } const getPriorityBadge = (priority: string) => { + const labels: Record = { + high: t("ticketsPage.priority.high"), + medium: t("ticketsPage.priority.medium"), + low: t("ticketsPage.priority.low"), + } switch (priority) { - case "high": return High - case "medium": return Medium - case "low": return Low + case "high": return {labels.high} + case "medium": return {labels.medium} + case "low": return {labels.low} default: return {priority} } } + // Skeleton loader component + const SkeletonRow = () => ( + + + + + + + + + + ) + return (
-

Support Tickets (Admin)

-

Manage all user support tickets.

+

{t("ticketsPage.title")}

+

{t("ticketsPage.description")}

+
- All Tickets - A list of all support tickets from users. + {t("ticketsPage.allTickets")} + {t("ticketsPage.allTicketsDescription")} - ID - Subject - Status - Priority - Created - Actions + {t("ticketsPage.table.id")} + {t("ticketsPage.table.subject")} + {t("ticketsPage.table.user")} + {t("ticketsPage.table.status")} + {t("ticketsPage.table.priority")} + {t("ticketsPage.table.created")} + {t("ticketsPage.table.actions")} {loading ? ( + <> + + + + + + + ) : paginatedTickets.length === 0 ? ( - Loading... - - ) : tickets.length === 0 ? ( - - No tickets found + + {t("ticketsPage.noTickets")} + ) : ( - tickets.map((ticket) => ( + paginatedTickets.map((ticket) => ( {ticket.id.substring(0, 8)}... - {ticket.subject} + + {ticket.subject} + + + {(ticket as any).userFullName || (ticket as any).userName || ticket.userId?.substring(0, 8) + "..."} + {getStatusBadge(ticket.status)} {getPriorityBadge(ticket.priority)} {format(new Date(ticket.createdAt), "MMM d, yyyy")}
- @@ -147,20 +226,100 @@ export default function AdminTicketsPage() { )}
+ + {/* Pagination */} + {!loading && totalPages > 1 && ( +
+

+ {t("ticketsPage.pagination.showing", { + start: (currentPage - 1) * ITEMS_PER_PAGE + 1, + end: Math.min(currentPage * ITEMS_PER_PAGE, tickets.length), + total: tickets.length + })} +

+
+ + + {currentPage} / {totalPages} + + +
+
+ )}
+ {/* Delete Dialog */} !open && setDeleteId(null)}> - Delete Ticket + {t("ticketsPage.deleteDialog.title")} - Are you sure you want to delete this ticket? This action cannot be undone. + {t("ticketsPage.deleteDialog.description")} - - + + + + + + + {/* Create Dialog */} + + + + {t("ticketsPage.createDialog.title")} + + {t("ticketsPage.createDialog.description")} + + +
+
+ + setNewTicket(prev => ({ ...prev, subject: e.target.value }))} + placeholder={t("ticketsPage.createDialog.subjectPlaceholder")} + /> +
+
+ + +
+
+ + +
diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 98bf27a..9a3124b 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -875,5 +875,63 @@ "rejected": "Rejected" } } + }, + "ticketsPage": { + "title": "Support Tickets (Admin)", + "description": "Manage all user support tickets.", + "allTickets": "All Tickets", + "allTicketsDescription": "A list of all support tickets from users.", + "newTicket": "New Ticket", + "noTickets": "No tickets found", + "view": "View", + "delete": "Delete", + "table": { + "id": "ID", + "subject": "Subject", + "user": "User", + "status": "Status", + "priority": "Priority", + "created": "Created", + "actions": "Actions" + }, + "status": { + "open": "Open", + "inProgress": "In Progress", + "closed": "Closed" + }, + "priority": { + "high": "High", + "medium": "Medium", + "low": "Low" + }, + "pagination": { + "showing": "Showing {{start}} to {{end}} of {{total}} tickets" + }, + "deleteDialog": { + "title": "Delete Ticket", + "description": "Are you sure you want to delete this ticket? This action cannot be undone.", + "cancel": "Cancel", + "confirm": "Delete" + }, + "createDialog": { + "title": "Create New Ticket", + "description": "Fill in the details to create a new support ticket.", + "subject": "Subject", + "subjectPlaceholder": "Describe your issue briefly", + "priority": "Priority", + "cancel": "Cancel", + "create": "Create Ticket", + "creating": "Creating..." + }, + "messages": { + "created": "Ticket created successfully", + "deleted": "Ticket deleted successfully" + }, + "errors": { + "fetchFailed": "Failed to fetch tickets", + "deleteFailed": "Failed to delete ticket", + "createFailed": "Failed to create ticket", + "subjectRequired": "Subject is required" + } } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index e0724cc..6232010 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -876,5 +876,63 @@ "rejected": "Rechazado" } } + }, + "ticketsPage": { + "title": "Tickets de Soporte (Admin)", + "description": "Gestiona todos los tickets de soporte de usuarios.", + "allTickets": "Todos los Tickets", + "allTicketsDescription": "Una lista de todos los tickets de soporte de usuarios.", + "newTicket": "Nuevo Ticket", + "noTickets": "No se encontraron tickets", + "view": "Ver", + "delete": "Eliminar", + "table": { + "id": "ID", + "subject": "Asunto", + "user": "Usuario", + "status": "Estado", + "priority": "Prioridad", + "created": "Creado", + "actions": "Acciones" + }, + "status": { + "open": "Abierto", + "inProgress": "En Progreso", + "closed": "Cerrado" + }, + "priority": { + "high": "Alta", + "medium": "Media", + "low": "Baja" + }, + "pagination": { + "showing": "Mostrando {{start}} a {{end}} de {{total}} tickets" + }, + "deleteDialog": { + "title": "Eliminar Ticket", + "description": "¿Estás seguro de que quieres eliminar este ticket? Esta acción no se puede deshacer.", + "cancel": "Cancelar", + "confirm": "Eliminar" + }, + "createDialog": { + "title": "Crear Nuevo Ticket", + "description": "Completa los detalles para crear un nuevo ticket de soporte.", + "subject": "Asunto", + "subjectPlaceholder": "Describe brevemente tu problema", + "priority": "Prioridad", + "cancel": "Cancelar", + "create": "Crear Ticket", + "creating": "Creando..." + }, + "messages": { + "created": "Ticket creado exitosamente", + "deleted": "Ticket eliminado exitosamente" + }, + "errors": { + "fetchFailed": "Error al obtener tickets", + "deleteFailed": "Error al eliminar ticket", + "createFailed": "Error al crear ticket", + "subjectRequired": "El asunto es requerido" + } } -} +} \ No newline at end of file diff --git a/frontend/src/i18n/pt-BR.json b/frontend/src/i18n/pt-BR.json index 2f851dd..877d532 100644 --- a/frontend/src/i18n/pt-BR.json +++ b/frontend/src/i18n/pt-BR.json @@ -875,5 +875,63 @@ "rejected": "Reprovado" } } + }, + "ticketsPage": { + "title": "Tickets de Suporte (Admin)", + "description": "Gerencie todos os tickets de suporte dos usuários.", + "allTickets": "Todos os Tickets", + "allTicketsDescription": "Uma lista de todos os tickets de suporte dos usuários.", + "newTicket": "Novo Ticket", + "noTickets": "Nenhum ticket encontrado", + "view": "Ver", + "delete": "Excluir", + "table": { + "id": "ID", + "subject": "Assunto", + "user": "Usuário", + "status": "Status", + "priority": "Prioridade", + "created": "Criado", + "actions": "Ações" + }, + "status": { + "open": "Aberto", + "inProgress": "Em Andamento", + "closed": "Fechado" + }, + "priority": { + "high": "Alta", + "medium": "Média", + "low": "Baixa" + }, + "pagination": { + "showing": "Mostrando {{start}} a {{end}} de {{total}} tickets" + }, + "deleteDialog": { + "title": "Excluir Ticket", + "description": "Tem certeza de que deseja excluir este ticket? Esta ação não pode ser desfeita.", + "cancel": "Cancelar", + "confirm": "Excluir" + }, + "createDialog": { + "title": "Criar Novo Ticket", + "description": "Preencha os detalhes para criar um novo ticket de suporte.", + "subject": "Assunto", + "subjectPlaceholder": "Descreva brevemente seu problema", + "priority": "Prioridade", + "cancel": "Cancelar", + "create": "Criar Ticket", + "creating": "Criando..." + }, + "messages": { + "created": "Ticket criado com sucesso", + "deleted": "Ticket excluído com sucesso" + }, + "errors": { + "fetchFailed": "Erro ao buscar tickets", + "deleteFailed": "Erro ao excluir ticket", + "createFailed": "Erro ao criar ticket", + "subjectRequired": "O assunto é obrigatório" + } } -} +} \ No newline at end of file