Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 108 additions & 2 deletions app/admin/support/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
User,
Calendar,
MessageSquare,
Save
Save,
Send
} from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
Expand Down Expand Up @@ -47,6 +48,8 @@ export default function TicketDetailPage() {
const [loading, setLoading] = useState(true)
const [updating, setUpdating] = useState(false)
const [notes, setNotes] = useState('')
const [reply, setReply] = useState('')
const [sendingReply, setSendingReply] = useState(false)

useEffect(() => {
fetchTicket()
Expand Down Expand Up @@ -94,6 +97,59 @@ export default function TicketDetailPage() {
}
}

const sendReply = async () => {
if (!reply.trim()) {
toast.error('Please enter a reply message')
return
}

setSendingReply(true)
try {
const response = await fetch(`/api/admin/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('')

// Update status to "in_progress" if it's "open"
if (ticket?.status === 'open') {
try {
const statusResponse = await fetch(`/api/admin/support/tickets/${ticketId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: 'in_progress' }),
})

if (statusResponse.ok) {
toast.success('Status updated to In Progress')
} else {
console.error('Failed to update status')
toast.error('Reply sent, but failed to update status')
}
} catch (statusError) {
console.error('Error updating status:', statusError)
toast.error('Reply sent, but failed to update status')
}
}

// Refresh ticket data
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'
Expand Down Expand Up @@ -179,6 +235,56 @@ export default function TicketDetailPage() {
</CardContent>
</Card>

{/* Reply to User */}
<Card className="border-blue-500/20 bg-blue-500/5">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Send className="h-5 w-5 text-blue-500" />
Reply to User
</CardTitle>
<CardDescription>
Send a response directly to {ticket.user?.email || 'the user'}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Textarea
placeholder="Type your response here... This will be sent via email to the user."
value={reply}
onChange={(e) => setReply(e.target.value)}
rows={6}
className="resize-none"
disabled={sendingReply}
/>
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">
{reply.length}/2000 characters
</p>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setReply('')}
disabled={!reply.trim() || sendingReply}
>
Clear
</Button>
<Button
onClick={sendReply}
disabled={!reply.trim() || sendingReply || reply.length > 2000}
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700"
>
<Send className="h-4 w-4 mr-2" />
{sendingReply ? 'Sending...' : 'Send Reply'}
</Button>
</div>
</div>
{ticket.status === 'open' && (
<p className="text-xs text-muted-foreground bg-yellow-500/10 border border-yellow-500/20 rounded p-2">
💡 Tip: Sending a reply will automatically change the status to &quot;In Progress&quot;
</p>
)}
</CardContent>
</Card>

{/* Internal Notes */}
<Card>
<CardHeader>
Expand All @@ -187,7 +293,7 @@ export default function TicketDetailPage() {
Internal Notes
</CardTitle>
<CardDescription>
Add notes visible only to admins
Add notes visible only to admins (not sent to user)
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
Expand Down
213 changes: 213 additions & 0 deletions app/api/admin/support/tickets/[id]/reply/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { sendEmail } from '@/lib/email/support-emails'

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params

console.log('📧 Reply API called for ticket:', id)

try {
const supabase = await createClient()

// Check if user is admin
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')
.eq('id', user.id)
.single()

if (!profile?.is_admin) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

const { message } = await request.json()

if (!message || !message.trim()) {
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
}

if (message.length > 2000) {
return NextResponse.json({ error: 'Message too long (max 2000 characters)' }, { status: 400 })
}

// Get ticket
const { data: ticket, error: ticketError } = await supabase
.from('support_tickets')
.select('*')
.eq('id', id)
.single()

if (ticketError) {
console.error('Error fetching ticket:', ticketError)
return NextResponse.json({ error: 'Ticket not found', details: ticketError.message }, { status: 404 })
}

if (!ticket) {
console.error('Ticket not found in database:', id)
return NextResponse.json({ error: 'Ticket not found' }, { status: 404 })
}

// Get user information
const { data: userProfile } = await supabase
.from('profiles')
.select('email, first_name, last_name')
.eq('id', ticket.user_id)
.single()

if (!userProfile?.email) {
console.error('User email not found for ticket:', id)
return NextResponse.json({ error: 'User email not found' }, { status: 400 })
}

console.log('✅ Ticket found:', { id: ticket.id, userEmail: userProfile.email })

// Prepare email
const userName = userProfile.first_name || userProfile.email.split('@')[0] || 'User'
const adminName = `${profile.first_name || ''} ${profile.last_name || ''}`.trim() || 'Support Team'

const emailHtml = getAdminReplyEmail({
userName,
adminName,
ticketId: ticket.id,
ticketSubject: ticket.subject,
replyMessage: message
})

// Send email to user
console.log('📧 Sending reply email to:', userProfile.email)

const emailResult = await sendEmail({
to: userProfile.email,
subject: `Re: [Ticket #${ticket.id.substring(0, 8)}] ${ticket.subject}`,
html: emailHtml
})

if (!emailResult.success) {
console.error('❌ Failed to send reply email:', emailResult.error)
return NextResponse.json({ error: 'Failed to send email', details: emailResult.error }, { status: 500 })
}

console.log('✅ Reply email sent successfully')

// TODO: Save reply to database (for reply history)
// This would go in a support_ticket_replies table

return NextResponse.json({
success: true,
message: 'Reply sent successfully'
})
} catch (error) {
console.error('Error in reply API:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

// Email template for admin reply
function getAdminReplyEmail(params: {
userName: string
adminName: string
ticketId: string
ticketSubject: string
replyMessage: string
}) {
const content = `
<h2 style="margin: 0 0 20px 0; color: #111827; font-size: 20px;">
Response to your support ticket
</h2>

<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
Hi ${params.userName},
</p>

<p style="margin: 0 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
${params.adminName} from our support team has responded to your ticket:
</p>

<div style="background-color: #f9fafb; border-left: 4px solid #3b82f6; padding: 15px; margin: 20px 0; border-radius: 4px;">
<p style="margin: 0 0 10px 0; color: #6b7280; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">
Ticket ID: ${params.ticketId}
</p>
<p style="margin: 0 0 5px 0; color: #111827; font-size: 16px; font-weight: 600;">
${params.ticketSubject}
</p>
</div>

<div style="background-color: #eff6ff; border: 1px solid #dbeafe; padding: 20px; margin: 20px 0; border-radius: 8px;">
<p style="margin: 0 0 10px 0; color: #1e40af; font-size: 14px; font-weight: 600;">
${params.adminName} replied:
</p>
<p style="margin: 0; color: #1f2937; font-size: 15px; line-height: 1.6; white-space: pre-wrap;">
${params.replyMessage}
</p>
</div>

<p style="margin: 20px 0 15px 0; color: #374151; font-size: 16px; line-height: 1.5;">
If you have any follow-up questions, please reply to this email or create a new ticket.
</p>

<a href="https://codeunia.com/protected/help" style="display: inline-block; background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 6px; font-weight: 600; margin-top: 10px;">
View Help Center
</a>
`

return getEmailTemplate(content)
}

// Base email template
function getEmailTemplate(content: string) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Codeunia Support</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f5f5f5; padding: 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); padding: 30px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 24px; font-weight: bold;">Codeunia Support</h1>
</td>
</tr>

<!-- Content -->
<tr>
<td style="padding: 40px 30px;">
${content}
</td>
</tr>

<!-- Footer -->
<tr>
<td style="background-color: #f9fafb; padding: 20px 30px; text-align: center; border-top: 1px solid #e5e7eb;">
<p style="margin: 0 0 10px 0; color: #6b7280; font-size: 14px;">
Need help? Reply to this email or visit our <a href="https://codeunia.com/protected/help" style="color: #3b82f6; text-decoration: none;">Help Center</a>
</p>
<p style="margin: 0; color: #9ca3af; font-size: 12px;">
© ${new Date().getFullYear()} Codeunia. All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`
}
Loading
Loading