diff --git a/app/admin/support/[id]/page.tsx b/app/admin/support/[id]/page.tsx index f69e7281..5f163776 100644 --- a/app/admin/support/[id]/page.tsx +++ b/app/admin/support/[id]/page.tsx @@ -24,7 +24,8 @@ import Link from 'next/link' interface TicketReply { id: string ticket_id: string - admin_id: string + admin_id?: string + user_id?: string message: string created_at: string updated_at: string @@ -35,6 +36,13 @@ interface TicketReply { last_name?: string avatar_url?: string } + user?: { + id: string + email: string + first_name?: string + last_name?: string + avatar_url?: string + } } interface SupportTicket { @@ -265,38 +273,49 @@ export default function TicketDetailPage() { - {ticket.replies.map((reply, index) => ( -
-
-
-
- {reply.admin?.first_name?.[0] || reply.admin?.email[0].toUpperCase() || 'A'} -
-
-

- {reply.admin?.first_name && reply.admin?.last_name - ? `${reply.admin.first_name} ${reply.admin.last_name}` - : reply.admin?.email || 'Admin'} -

-

- {new Date(reply.created_at).toLocaleString()} -

+ {ticket.replies.map((reply, index) => { + const isAdminReply = !!reply.admin_id + const author = isAdminReply ? reply.admin : reply.user + const authorName = author?.first_name && author?.last_name + ? `${author.first_name} ${author.last_name}` + : author?.email || (isAdminReply ? 'Admin' : 'User') + const authorInitial = author?.first_name?.[0] || author?.email?.[0]?.toUpperCase() || (isAdminReply ? 'A' : 'U') + const bgGradient = isAdminReply + ? 'from-purple-500 to-blue-600' + : 'from-blue-500 to-green-600' + + return ( +
+
+
+
+ {authorInitial} +
+
+

+ {authorName} + {!isAdminReply && (User)} +

+

+ {new Date(reply.created_at).toLocaleString()} +

+
+ + Reply #{index + 1} + +
+
+

+ {reply.message} +

- - Reply #{index + 1} - -
-
-

- {reply.message} -

-
- ))} + ) + })} )} diff --git a/app/api/admin/support/tickets/[id]/route.ts b/app/api/admin/support/tickets/[id]/route.ts index 9dce0a54..54af08b5 100644 --- a/app/api/admin/support/tickets/[id]/route.ts +++ b/app/api/admin/support/tickets/[id]/route.ts @@ -55,24 +55,28 @@ export async function GET( .eq('ticket_id', id) .order('created_at', { ascending: true }) - // Get admin profiles for replies - const adminIds = [...new Set(replies?.map(r => r.admin_id) || [])] - const { data: adminProfiles } = await supabase + // Get admin and user profiles for replies + const adminIds = [...new Set(replies?.map(r => r.admin_id).filter(id => id) || [])] + const userIds = [...new Set(replies?.map(r => r.user_id).filter(id => id) || [])] + const allProfileIds = [...adminIds, ...userIds] + + const { data: profiles } = await supabase .from('profiles') .select('id, email, first_name, last_name, avatar_url') - .in('id', adminIds) + .in('id', allProfileIds) - // Map admin data to replies - const repliesWithAdmins = replies?.map(reply => ({ + // Map admin and user data to replies + const repliesWithAuthors = replies?.map(reply => ({ ...reply, - admin: adminProfiles?.find(p => p.id === reply.admin_id) || null + admin: reply.admin_id ? profiles?.find(p => p.id === reply.admin_id) || null : null, + user: reply.user_id ? profiles?.find(p => p.id === reply.user_id) || null : null })) || [] // Combine ticket with user data and replies const ticketWithUser = { ...ticket, user: userProfile || null, - replies: repliesWithAdmins + replies: repliesWithAuthors } return NextResponse.json({ ticket: ticketWithUser }) diff --git a/app/api/support/tickets/[id]/reply/route.ts b/app/api/support/tickets/[id]/reply/route.ts new file mode 100644 index 00000000..77a1f3f4 --- /dev/null +++ b/app/api/support/tickets/[id]/reply/route.ts @@ -0,0 +1,136 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createClient } from '@/lib/supabase/server' +import { sendEmail, getAdminReplyEmail, getSupportTeamNotificationEmail } from '@/lib/email/support-emails' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params + + try { + const supabase = await createClient() + + const { data: { user }, error: authError } = await supabase.auth.getUser() + + if (authError || !user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { data: profile } = await supabase + .from('profiles') + .select('is_admin, first_name, last_name, email') + .eq('id', user.id) + .single() + + if (!profile) { + return NextResponse.json({ error: 'Profile not found' }, { status: 404 }) + } + + const { message } = await request.json() + + if (!message || !message.trim()) { + return NextResponse.json({ error: 'Message is required' }, { status: 400 }) + } + + const { data: ticket, error: ticketError } = await supabase + .from('support_tickets') + .select('*') + .eq('id', id) + .single() + + if (ticketError || !ticket) { + return NextResponse.json({ error: 'Ticket not found' }, { status: 404 }) + } + + if (profile.is_admin) { + // Admin reply logic + const { data: userProfile } = await supabase + .from('profiles') + .select('email, first_name') + .eq('id', ticket.user_id) + .single() + + if (!userProfile?.email) { + return NextResponse.json({ error: 'User email not found' }, { status: 400 }) + } + + const userName = userProfile.first_name || 'User' + const adminName = `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || 'Support Team' + + const { subject, html } = getAdminReplyEmail({ + userName, + adminName, + ticketId: ticket.id, + ticketSubject: ticket.subject, + replyMessage: message + }) + + await sendEmail({ + to: userProfile.email, + subject, + html, + }) + + const { data: reply, error: replyError } = await supabase + .from('support_ticket_replies') + .insert({ + ticket_id: ticket.id, + admin_id: user.id, + message: message.trim() + }) + .select() + .single() + + if (replyError) { + console.error('Error saving admin reply:', replyError) + return NextResponse.json({ error: 'Failed to save reply' }, { status: 500 }) + } + + return NextResponse.json({ success: true, reply }) + + } else { + // User reply logic + if (ticket.user_id !== user.id) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const supportEmail = process.env.SUPPORT_EMAIL + if (supportEmail) { + const { subject, html } = getSupportTeamNotificationEmail({ + ticketId: ticket.id, + ticketType: ticket.type, + subject: `New reply on: ${ticket.subject}`, + message, + userEmail: profile.email, + userName: profile.first_name || 'User', + }) + await sendEmail({ + to: supportEmail, + subject, + html, + }) + } + + const { data: reply, error: replyError } = await supabase + .from('support_ticket_replies') + .insert({ + ticket_id: ticket.id, + user_id: user.id, + message: message.trim() + }) + .select() + .single() + + if (replyError) { + console.error('Error saving user reply:', replyError) + return NextResponse.json({ error: 'Failed to save reply' }, { status: 500 }) + } + + return NextResponse.json({ success: true, reply }) + } + } catch (error) { + console.error('Error in POST ticket reply:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/app/api/support/tickets/[id]/route.ts b/app/api/support/tickets/[id]/route.ts index 85270a71..72781ad8 100644 --- a/app/api/support/tickets/[id]/route.ts +++ b/app/api/support/tickets/[id]/route.ts @@ -32,24 +32,27 @@ export async function GET( const { data: replies } = await supabase .from('support_ticket_replies') - .select('id, admin_id, message, created_at') + .select('id, admin_id, user_id, message, created_at') .eq('ticket_id', id) .order('created_at', { ascending: true }) - const adminIds = [...new Set(replies?.map(r => r.admin_id) || [])] - const { data: adminProfiles } = await supabase + const adminIds = [...new Set(replies?.map(r => r.admin_id).filter(id => id) || [])] + const userIds = [...new Set(replies?.map(r => r.user_id).filter(id => id) || [])] + + const { data: profiles } = await supabase .from('profiles') .select('id, first_name, last_name, avatar_url') - .in('id', adminIds) + .in('id', [...adminIds, ...userIds]) - const repliesWithAdmins = replies?.map(reply => ({ + const repliesWithAuthors = replies?.map(reply => ({ ...reply, - admin: adminProfiles?.find(p => p.id === reply.admin_id) || null + admin: profiles?.find(p => p.id === reply.admin_id) || null, + user: profiles?.find(p => p.id === reply.user_id) || null })) || [] const ticketWithReplies = { ...ticket, - replies: repliesWithAdmins + replies: repliesWithAuthors } return NextResponse.json({ ticket: ticketWithReplies }) diff --git a/app/protected/help/ticket/[id]/page.tsx b/app/protected/help/ticket/[id]/page.tsx index 55494e11..80a6a924 100644 --- a/app/protected/help/ticket/[id]/page.tsx +++ b/app/protected/help/ticket/[id]/page.tsx @@ -2,24 +2,31 @@ import React, { useState, useEffect } from 'react' import { useParams, useRouter } from 'next/navigation' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent, 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 { ArrowLeft, MessageSquare, Clock, Calendar, Bug, Mail } from 'lucide-react' +import { Textarea } from '@/components/ui/textarea' +import { ArrowLeft, MessageSquare, Bug, Mail, User, PlusCircle, XCircle, Send } from 'lucide-react' import { toast } from 'sonner' import Link from 'next/link' interface TicketReply { id: string - admin_id: string message: string created_at: string + admin_id?: string + user_id?: string admin?: { first_name?: string last_name?: string avatar_url?: string } + user?: { + first_name?: string + last_name?: string + avatar_url?: string + } } interface SupportTicket { @@ -40,6 +47,9 @@ export default function UserTicketDetailPage() { const [ticket, setTicket] = useState(null) const [loading, setLoading] = useState(true) + const [reply, setReply] = useState('') + const [sendingReply, setSendingReply] = useState(false) + const [replyError, setReplyError] = useState('') useEffect(() => { if (ticketId) { @@ -66,6 +76,37 @@ export default function UserTicketDetailPage() { } } + const handleReplySubmit = async () => { + if (!reply.trim()) { + setReplyError('Reply message cannot be empty.') + return + } + setReplyError('') + setSendingReply(true) + + try { + const response = await fetch(`/api/support/tickets/${ticketId}/reply`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: reply }), + }) + + if (response.ok) { + toast.success('Reply sent successfully!') + setReply('') + await fetchTicket() + } else { + const error = await response.json() + toast.error(error.error || 'Failed to send reply') + } + } catch (error) { + console.error('Error sending reply:', error) + toast.error('Failed to send reply') + } finally { + setSendingReply(false) + } + } + const getStatusColor = (status: string) => { switch (status) { case 'open': return 'bg-red-500/10 text-red-400 border-red-500/20' @@ -80,7 +121,15 @@ export default function UserTicketDetailPage() { return (
- +
+
+ + +
+
+ +
+
) } @@ -109,81 +158,166 @@ export default function UserTicketDetailPage() {
-

Ticket Details

-

ID: {ticket.id}

+

+ {ticket.type === 'bug' ? ( + + ) : ( + + )} + {ticket.subject} +

{ticket.status.replace('_', ' ')}
- - -
- {ticket.type === 'bug' ? ( - - ) : ( - - )} - {ticket.subject} -
- - {ticket.type === 'bug' ? 'Bug Report' : 'Support Request'} - -
- -
-

{ticket.message}

-
-
-
- - Created: {new Date(ticket.created_at).toLocaleString()} -
-
- - Last updated: {new Date(ticket.updated_at).toLocaleString()} -
-
-
-
+
+
+ {/* Original Message */} + + + + + Your Initial Request + + + +

{ticket.message}

+
+
- {ticket.replies && ticket.replies.length > 0 && ( - - - - - Reply History - - - - {ticket.replies.map((reply) => ( -
-
-
- {reply.admin?.first_name?.[0] || 'A'} -
-
-

- {reply.admin?.first_name && reply.admin?.last_name - ? `${reply.admin.first_name} ${reply.admin.last_name}` - : 'Support Team'} -

-

- {new Date(reply.created_at).toLocaleString()} -

+ {/* Reply History */} + {ticket.replies && ticket.replies.length > 0 && ( + + + + + Activity + + + + {ticket.replies.map((reply) => ( +
+
+ {reply.admin_id + ? reply.admin?.first_name?.[0] || 'S' + : reply.user?.first_name?.[0] || 'U' + } +
+
+
+

+ {reply.admin_id + ? (reply.admin?.first_name && reply.admin?.last_name + ? `${reply.admin.first_name} ${reply.admin.last_name}` + : 'Support Team') + : (reply.user?.first_name && reply.user?.last_name + ? `${reply.user.first_name} ${reply.user.last_name}` + : 'You') + } +

+

+ {new Date(reply.created_at).toLocaleString()} +

+
+
+

{reply.message}

+
+
+ ))} +
+
+ )} + + {/* Reply Form */} + {ticket.status !== 'resolved' && ticket.status !== 'closed' && ( + + + + + Send a Reply + + + +