- Place Category and Priority side by side in create ticket dialogs
- Fix pagination showing literal {1} instead of actual values
- Fix double-brace interpolation in en.json (tickets, companies)
- Replace Add Job modal on dashboard with link to /dashboard/jobs/new
215 lines
11 KiB
TypeScript
215 lines
11 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Ticket, ticketsApi } from "@/lib/api"
|
|
import { toast } from "sonner"
|
|
import Link from "next/link"
|
|
import { Plus, MessageSquare } from "lucide-react"
|
|
import { format } from "date-fns"
|
|
|
|
export default function TicketsPage() {
|
|
const [tickets, setTickets] = useState<Ticket[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [isCreateOpen, setIsCreateOpen] = useState(false)
|
|
const [newTicket, setNewTicket] = useState({ subject: "", category: "support", priority: "medium", message: "" })
|
|
|
|
const fetchTickets = async () => {
|
|
try {
|
|
const data = await ticketsApi.list()
|
|
setTickets(data)
|
|
setLoading(false)
|
|
} catch (error) {
|
|
console.error("Failed to fetch tickets", error)
|
|
toast.error("Failed to fetch tickets")
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchTickets()
|
|
}, [])
|
|
|
|
const handleCreateTicket = async () => {
|
|
if (!newTicket.subject || !newTicket.message) return
|
|
|
|
try {
|
|
await ticketsApi.create(newTicket)
|
|
toast.success("Ticket created successfully")
|
|
setIsCreateOpen(false)
|
|
setNewTicket({ subject: "", category: "support", priority: "medium", message: "" })
|
|
fetchTickets() // Refresh
|
|
} catch (error) {
|
|
console.error("Failed to create ticket", error)
|
|
toast.error("Failed to create ticket")
|
|
}
|
|
}
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
switch (status) {
|
|
case "open": return <Badge variant="default" className="bg-green-500">Open</Badge>
|
|
case "in_progress": return <Badge variant="secondary" className="bg-yellow-500 text-white">In Progress</Badge>
|
|
case "closed": return <Badge variant="outline">Closed</Badge>
|
|
default: return <Badge variant="outline">{status}</Badge>
|
|
}
|
|
}
|
|
|
|
const getPriorityBadge = (priority: string) => {
|
|
switch (priority) {
|
|
case "high": return <Badge variant="destructive">High</Badge>
|
|
case "medium": return <Badge variant="secondary">Medium</Badge>
|
|
case "low": return <Badge variant="outline">Low</Badge>
|
|
default: return <Badge variant="outline">{priority}</Badge>
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Support Tickets</h1>
|
|
<p className="text-muted-foreground">Manage your support requests and inquiries.</p>
|
|
</div>
|
|
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
New Ticket
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Ticket</DialogTitle>
|
|
<DialogDescription>Describe your issue and priority.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="subject">Subject</Label>
|
|
<Input
|
|
id="subject"
|
|
placeholder="Describe your issue briefly"
|
|
value={newTicket.subject}
|
|
onChange={(e) => setNewTicket({ ...newTicket, subject: e.target.value })}
|
|
/>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="category">Category</Label>
|
|
<Select
|
|
value={newTicket.category}
|
|
onValueChange={(val) => setNewTicket({ ...newTicket, category: val })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select category" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="bug">Bug</SelectItem>
|
|
<SelectItem value="feature">Feature Request</SelectItem>
|
|
<SelectItem value="support">Support</SelectItem>
|
|
<SelectItem value="billing">Billing</SelectItem>
|
|
<SelectItem value="other">Other</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="priority">Priority</Label>
|
|
<Select
|
|
value={newTicket.priority}
|
|
onValueChange={(val) => setNewTicket({ ...newTicket, priority: val })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select priority" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="low">Low</SelectItem>
|
|
<SelectItem value="medium">Medium</SelectItem>
|
|
<SelectItem value="high">High</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="message">Message</Label>
|
|
<Textarea
|
|
id="message"
|
|
placeholder="Describe your request in detail"
|
|
value={newTicket.message}
|
|
onChange={(e) => setNewTicket({ ...newTicket, message: e.target.value })}
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button onClick={handleCreateTicket}>Create Ticket</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>My Tickets</CardTitle>
|
|
<CardDescription>A list of your recent support tickets.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>ID</TableHead>
|
|
<TableHead>Subject</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Priority</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="text-center py-4">Loading...</TableCell>
|
|
</TableRow>
|
|
) : tickets.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="text-center py-4">No tickets found</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
tickets.map((ticket) => (
|
|
<TableRow key={ticket.id}>
|
|
<TableCell className="font-mono text-xs max-w-[80px] truncate">{ticket.id.substring(0, 8)}...</TableCell>
|
|
<TableCell className="font-medium">{ticket.subject}</TableCell>
|
|
<TableCell>{getStatusBadge(ticket.status)}</TableCell>
|
|
<TableCell>{getPriorityBadge(ticket.priority)}</TableCell>
|
|
<TableCell>{format(new Date(ticket.createdAt), "MMM d, yyyy")}</TableCell>
|
|
<TableCell className="text-right">
|
|
<Link href={`./tickets/${ticket.id}`}>
|
|
<Button variant="ghost" size="sm">
|
|
<MessageSquare className="h-4 w-4 mr-2" />
|
|
View
|
|
</Button>
|
|
</Link>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|