From 1e390ed0903cd8c1fea69bffa93e8dbdc72f0fcc Mon Sep 17 00:00:00 2001 From: Akshay Date: Fri, 7 Nov 2025 10:35:09 +0530 Subject: [PATCH 1/2] feat(admin): Add comprehensive support ticket management system - Implement new support ticket detail page with dynamic routing - Add support ticket API routes for fetching and updating tickets - Enhance admin sidebar with new Support Tickets navigation item - Create detailed ticket view with status tracking and user information - Implement loading skeletons and error handling for ticket management - Add internal notes section for admin communication - Support different ticket types (bug report and contact request) - Integrate toast notifications for ticket status updates --- app/admin/layout.tsx | 6 + app/admin/support/[id]/page.tsx | 314 +++++++++++++++++ app/admin/support/page.tsx | 371 ++++++++++++++++++++ app/api/admin/support/tickets/[id]/route.ts | 109 ++++++ app/api/admin/support/tickets/route.ts | 54 +++ 5 files changed, 854 insertions(+) create mode 100644 app/admin/support/[id]/page.tsx create mode 100644 app/admin/support/page.tsx create mode 100644 app/api/admin/support/tickets/[id]/route.ts create mode 100644 app/api/admin/support/tickets/route.ts diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index f0069334..b142e5a6 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -23,6 +23,7 @@ import { ClipboardCheck, Award, Crown, + LifeBuoy, } from "lucide-react" import { useAuth } from "@/lib/hooks/useAuth" @@ -165,6 +166,11 @@ const sidebarItems: SidebarGroupType[] = [ { title: "Support", items: [ + { + title: "Support Tickets", + url: "/admin/support", + icon: LifeBuoy, + }, { title: "Messages", url: "/admin/messages", diff --git a/app/admin/support/[id]/page.tsx b/app/admin/support/[id]/page.tsx new file mode 100644 index 00000000..103d1b4e --- /dev/null +++ b/app/admin/support/[id]/page.tsx @@ -0,0 +1,314 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Skeleton } from '@/components/ui/skeleton' +import { Textarea } from '@/components/ui/textarea' +import { + ArrowLeft, + Mail, + Bug, + Clock, + User, + Calendar, + MessageSquare, + Save +} from 'lucide-react' +import { toast } from 'sonner' +import Link from 'next/link' + +interface SupportTicket { + id: string + user_id: string + type: 'contact' | 'bug' + subject: string + message: string + status: 'open' | 'in_progress' | 'resolved' | 'closed' + created_at: string + updated_at: string + user?: { + id: string + email: string + first_name?: string + last_name?: string + avatar_url?: string + } +} + +export default function TicketDetailPage() { + const params = useParams() + const router = useRouter() + const ticketId = params.id as string + + const [ticket, setTicket] = useState(null) + const [loading, setLoading] = useState(true) + const [updating, setUpdating] = useState(false) + const [notes, setNotes] = useState('') + + useEffect(() => { + fetchTicket() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ticketId]) + + const fetchTicket = async () => { + try { + const response = await fetch(`/api/admin/support/tickets/${ticketId}`) + if (response.ok) { + const data = await response.json() + setTicket(data.ticket) + } else { + toast.error('Failed to load ticket') + router.push('/admin/support') + } + } catch (error) { + console.error('Error fetching ticket:', error) + toast.error('Failed to load ticket') + } finally { + setLoading(false) + } + } + + const updateStatus = async (newStatus: string) => { + setUpdating(true) + try { + const response = await fetch(`/api/admin/support/tickets/${ticketId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }), + }) + + if (response.ok) { + toast.success('Status updated successfully') + fetchTicket() + } else { + toast.error('Failed to update status') + } + } catch (error) { + console.error('Error updating status:', error) + toast.error('Failed to update status') + } finally { + setUpdating(false) + } + } + + const getStatusColor = (status: string) => { + switch (status) { + case 'open': return 'bg-red-500/10 text-red-400 border-red-500/20' + case 'in_progress': return 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20' + case 'resolved': return 'bg-green-500/10 text-green-400 border-green-500/20' + case 'closed': return 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20' + default: return 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20' + } + } + + if (loading) { + return ( +
+ +
+
+ +
+
+ + +
+
+
+ ) + } + + if (!ticket) { + return ( +
+ + +

Ticket not found

+ +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+ +
+

Ticket Details

+

ID: {ticket.id}

+
+ + {ticket.status.replace('_', ' ')} + +
+ +
+ {/* Main Content */} +
+ {/* Ticket Info */} + + +
+ {ticket.type === 'bug' ? ( + + ) : ( + + )} + {ticket.subject} +
+ + {ticket.type === 'bug' ? 'Bug Report' : 'Support Request'} + +
+ +
+

{ticket.message}

+
+
+
+ + {/* Internal Notes */} + + + + + Internal Notes + + + Add notes visible only to admins + + + +