feat(tickets): add pagination, skeleton loading, create modal, i18n support
This commit is contained in:
parent
0c38ce1b5f
commit
1b9bd81687
4 changed files with 371 additions and 38 deletions
|
|
@ -15,7 +15,7 @@ import { Badge } from "@/components/ui/badge"
|
||||||
import { Ticket, ticketsApi } from "@/lib/api"
|
import { Ticket, ticketsApi } from "@/lib/api"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import Link from "next/link"
|
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 { format } from "date-fns"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -25,11 +25,31 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} 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() {
|
export default function AdminTicketsPage() {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [tickets, setTickets] = useState<Ticket[]>([])
|
const [tickets, setTickets] = useState<Ticket[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
const [deleteId, setDeleteId] = useState<string | null>(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 () => {
|
const fetchTickets = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -38,7 +58,7 @@ export default function AdminTicketsPage() {
|
||||||
setTickets(data)
|
setTickets(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch tickets", error)
|
console.error("Failed to fetch tickets", error)
|
||||||
toast.error("Failed to fetch tickets")
|
toast.error(t("ticketsPage.errors.fetchFailed"))
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -52,91 +72,150 @@ export default function AdminTicketsPage() {
|
||||||
if (!deleteId) return
|
if (!deleteId) return
|
||||||
try {
|
try {
|
||||||
await ticketsApi.delete(deleteId)
|
await ticketsApi.delete(deleteId)
|
||||||
toast.success("Ticket deleted")
|
toast.success(t("ticketsPage.messages.deleted"))
|
||||||
setDeleteId(null)
|
setDeleteId(null)
|
||||||
fetchTickets()
|
fetchTickets()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete ticket", 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 getStatusBadge = (status: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
open: t("ticketsPage.status.open"),
|
||||||
|
in_progress: t("ticketsPage.status.inProgress"),
|
||||||
|
closed: t("ticketsPage.status.closed"),
|
||||||
|
}
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "open": return <Badge variant="default" className="bg-green-500">Open</Badge>
|
case "open": return <Badge variant="default" className="bg-green-500">{labels.open}</Badge>
|
||||||
case "in_progress": return <Badge variant="secondary" className="bg-yellow-500 text-white">In Progress</Badge>
|
case "in_progress": return <Badge variant="secondary" className="bg-yellow-500 text-white">{labels.in_progress}</Badge>
|
||||||
case "closed": return <Badge variant="outline">Closed</Badge>
|
case "closed": return <Badge variant="outline">{labels.closed}</Badge>
|
||||||
default: return <Badge variant="outline">{status}</Badge>
|
default: return <Badge variant="outline">{status}</Badge>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPriorityBadge = (priority: string) => {
|
const getPriorityBadge = (priority: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
high: t("ticketsPage.priority.high"),
|
||||||
|
medium: t("ticketsPage.priority.medium"),
|
||||||
|
low: t("ticketsPage.priority.low"),
|
||||||
|
}
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case "high": return <Badge variant="destructive">High</Badge>
|
case "high": return <Badge variant="destructive">{labels.high}</Badge>
|
||||||
case "medium": return <Badge variant="secondary">Medium</Badge>
|
case "medium": return <Badge variant="secondary">{labels.medium}</Badge>
|
||||||
case "low": return <Badge variant="outline">Low</Badge>
|
case "low": return <Badge variant="outline">{labels.low}</Badge>
|
||||||
default: return <Badge variant="outline">{priority}</Badge>
|
default: return <Badge variant="outline">{priority}</Badge>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skeleton loader component
|
||||||
|
const SkeletonRow = () => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell><Skeleton className="h-4 w-16" /></TableCell>
|
||||||
|
<TableCell><Skeleton className="h-4 w-48" /></TableCell>
|
||||||
|
<TableCell><Skeleton className="h-4 w-24" /></TableCell>
|
||||||
|
<TableCell><Skeleton className="h-5 w-16" /></TableCell>
|
||||||
|
<TableCell><Skeleton className="h-5 w-14" /></TableCell>
|
||||||
|
<TableCell><Skeleton className="h-4 w-24" /></TableCell>
|
||||||
|
<TableCell className="text-right"><Skeleton className="h-8 w-16 ml-auto" /></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Support Tickets (Admin)</h1>
|
<h1 className="text-3xl font-bold tracking-tight">{t("ticketsPage.title")}</h1>
|
||||||
<p className="text-muted-foreground">Manage all user support tickets.</p>
|
<p className="text-muted-foreground">{t("ticketsPage.description")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<Button onClick={() => setCreateOpen(true)}>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
{t("ticketsPage.newTicket")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>All Tickets</CardTitle>
|
<CardTitle>{t("ticketsPage.allTickets")}</CardTitle>
|
||||||
<CardDescription>A list of all support tickets from users.</CardDescription>
|
<CardDescription>{t("ticketsPage.allTicketsDescription")}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>ID</TableHead>
|
<TableHead>{t("ticketsPage.table.id")}</TableHead>
|
||||||
<TableHead>Subject</TableHead>
|
<TableHead>{t("ticketsPage.table.subject")}</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>{t("ticketsPage.table.user")}</TableHead>
|
||||||
<TableHead>Priority</TableHead>
|
<TableHead>{t("ticketsPage.table.status")}</TableHead>
|
||||||
<TableHead>Created</TableHead>
|
<TableHead>{t("ticketsPage.table.priority")}</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead>{t("ticketsPage.table.created")}</TableHead>
|
||||||
|
<TableHead className="text-right">{t("ticketsPage.table.actions")}</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<SkeletonRow />
|
||||||
|
<SkeletonRow />
|
||||||
|
<SkeletonRow />
|
||||||
|
<SkeletonRow />
|
||||||
|
<SkeletonRow />
|
||||||
|
</>
|
||||||
|
) : paginatedTickets.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center py-4">Loading...</TableCell>
|
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
|
||||||
</TableRow>
|
{t("ticketsPage.noTickets")}
|
||||||
) : tickets.length === 0 ? (
|
</TableCell>
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={6} className="text-center py-4">No tickets found</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
tickets.map((ticket) => (
|
paginatedTickets.map((ticket) => (
|
||||||
<TableRow key={ticket.id}>
|
<TableRow key={ticket.id}>
|
||||||
<TableCell className="font-mono text-xs max-w-[80px] truncate" title={ticket.id}>
|
<TableCell className="font-mono text-xs max-w-[80px] truncate" title={ticket.id}>
|
||||||
{ticket.id.substring(0, 8)}...
|
{ticket.id.substring(0, 8)}...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-medium">{ticket.subject}</TableCell>
|
<TableCell className="font-medium max-w-[200px] truncate" title={ticket.subject}>
|
||||||
|
{ticket.subject}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm" title={ticket.userId}>
|
||||||
|
{(ticket as any).userFullName || (ticket as any).userName || ticket.userId?.substring(0, 8) + "..."}
|
||||||
|
</TableCell>
|
||||||
<TableCell>{getStatusBadge(ticket.status)}</TableCell>
|
<TableCell>{getStatusBadge(ticket.status)}</TableCell>
|
||||||
<TableCell>{getPriorityBadge(ticket.priority)}</TableCell>
|
<TableCell>{getPriorityBadge(ticket.priority)}</TableCell>
|
||||||
<TableCell>{format(new Date(ticket.createdAt), "MMM d, yyyy")}</TableCell>
|
<TableCell>{format(new Date(ticket.createdAt), "MMM d, yyyy")}</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Link href={`./tickets/${ticket.id}`}>
|
<Link href={`./tickets/${ticket.id}`}>
|
||||||
<Button variant="ghost" size="sm">
|
<Button variant="ghost" size="icon" title={t("ticketsPage.view")}>
|
||||||
<Eye className="h-4 w-4 mr-2" />
|
<Eye className="h-4 w-4" />
|
||||||
View
|
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="icon"
|
||||||
onClick={() => setDeleteId(ticket.id)}
|
onClick={() => setDeleteId(ticket.id)}
|
||||||
className="text-destructive hover:text-destructive"
|
className="text-destructive hover:text-destructive"
|
||||||
|
title={t("ticketsPage.delete")}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -147,20 +226,100 @@ export default function AdminTicketsPage() {
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{!loading && totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("ticketsPage.pagination.showing", {
|
||||||
|
start: (currentPage - 1) * ITEMS_PER_PAGE + 1,
|
||||||
|
end: Math.min(currentPage * ITEMS_PER_PAGE, tickets.length),
|
||||||
|
total: tickets.length
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<span className="flex items-center px-3 text-sm">
|
||||||
|
{currentPage} / {totalPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Delete Dialog */}
|
||||||
<Dialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
<Dialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Delete Ticket</DialogTitle>
|
<DialogTitle>{t("ticketsPage.deleteDialog.title")}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Are you sure you want to delete this ticket? This action cannot be undone.
|
{t("ticketsPage.deleteDialog.description")}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setDeleteId(null)}>Cancel</Button>
|
<Button variant="outline" onClick={() => setDeleteId(null)}>{t("ticketsPage.deleteDialog.cancel")}</Button>
|
||||||
<Button variant="destructive" onClick={handleDelete}>Delete</Button>
|
<Button variant="destructive" onClick={handleDelete}>{t("ticketsPage.deleteDialog.confirm")}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Create Dialog */}
|
||||||
|
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("ticketsPage.createDialog.title")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("ticketsPage.createDialog.description")}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="subject">{t("ticketsPage.createDialog.subject")}</Label>
|
||||||
|
<Input
|
||||||
|
id="subject"
|
||||||
|
value={newTicket.subject}
|
||||||
|
onChange={(e) => setNewTicket(prev => ({ ...prev, subject: e.target.value }))}
|
||||||
|
placeholder={t("ticketsPage.createDialog.subjectPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="priority">{t("ticketsPage.createDialog.priority")}</Label>
|
||||||
|
<Select
|
||||||
|
value={newTicket.priority}
|
||||||
|
onValueChange={(value) => setNewTicket(prev => ({ ...prev, priority: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="low">{t("ticketsPage.priority.low")}</SelectItem>
|
||||||
|
<SelectItem value="medium">{t("ticketsPage.priority.medium")}</SelectItem>
|
||||||
|
<SelectItem value="high">{t("ticketsPage.priority.high")}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setCreateOpen(false)}>{t("ticketsPage.createDialog.cancel")}</Button>
|
||||||
|
<Button onClick={handleCreate} disabled={creating}>
|
||||||
|
{creating ? t("ticketsPage.createDialog.creating") : t("ticketsPage.createDialog.create")}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -875,5 +875,63 @@
|
||||||
"rejected": "Rejected"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -876,5 +876,63 @@
|
||||||
"rejected": "Rechazado"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -875,5 +875,63 @@
|
||||||
"rejected": "Reprovado"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue