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
79 changes: 49 additions & 30 deletions app/admin/support/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -265,38 +273,49 @@ export default function TicketDetailPage() {
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{ticket.replies.map((reply, index) => (
<div
key={reply.id}
className="border-l-4 border-purple-500/30 bg-purple-500/5 rounded-r-lg p-4 space-y-2"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center text-white text-sm font-semibold">
{reply.admin?.first_name?.[0] || reply.admin?.email[0].toUpperCase() || 'A'}
</div>
<div>
<p className="text-sm font-medium">
{reply.admin?.first_name && reply.admin?.last_name
? `${reply.admin.first_name} ${reply.admin.last_name}`
: reply.admin?.email || 'Admin'}
</p>
<p className="text-xs text-muted-foreground">
{new Date(reply.created_at).toLocaleString()}
</p>
{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 (
<div
key={reply.id}
className={`border-l-4 ${isAdminReply ? 'border-purple-500/30 bg-purple-500/5' : 'border-blue-500/30 bg-blue-500/5'} rounded-r-lg p-4 space-y-2`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`h-8 w-8 rounded-full bg-gradient-to-br ${bgGradient} flex items-center justify-center text-white text-sm font-semibold`}>
{authorInitial}
</div>
<div>
<p className="text-sm font-medium">
{authorName}
{!isAdminReply && <span className="text-xs text-muted-foreground ml-2">(User)</span>}
</p>
<p className="text-xs text-muted-foreground">
{new Date(reply.created_at).toLocaleString()}
</p>
</div>
</div>
<Badge variant="outline" className="text-xs">
Reply #{index + 1}
</Badge>
</div>
<div className="pl-10">
<p className="text-sm text-foreground whitespace-pre-wrap">
{reply.message}
</p>
</div>
<Badge variant="outline" className="text-xs">
Reply #{index + 1}
</Badge>
</div>
<div className="pl-10">
<p className="text-sm text-foreground whitespace-pre-wrap">
{reply.message}
</p>
</div>
</div>
))}
)
})}
</CardContent>
</Card>
)}
Expand Down
20 changes: 12 additions & 8 deletions app/api/admin/support/tickets/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
136 changes: 136 additions & 0 deletions app/api/support/tickets/[id]/reply/route.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
}
17 changes: 10 additions & 7 deletions app/api/support/tickets/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
Loading
Loading