import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { Mail, Clock, CheckCircle2, XCircle, Filter, Search, ChevronDown, AlertCircle, Send, ArrowUpRight, ArrowDownLeft, Loader2, RefreshCw, Reply, Copy, Check, User, Award, TrendingUp, } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { useTheme } from '@/components/theme/ThemeProvider'; import { useUserRole } from '@/hooks/useUserRole'; import { handleError, handleSuccess } from '@/lib/errorHandler'; import { logger } from '@/lib/logger'; import { contactCategories } from '@/lib/contactValidation'; import { invokeWithTracking } from '@/lib/edgeFunctionTracking'; import { AdminLayout } from '@/components/layout/AdminLayout'; interface ContactSubmission { id: string; created_at: string; updated_at: string; user_id: string | null; submitter_username: string | null; submitter_reputation: number | null; submitter_profile_data: { display_name?: string; member_since?: string; stats?: { rides: number; coasters: number; parks: number; reviews: number; }; reputation?: number; avatar_url?: string; } | null; name: string; email: string; subject: string; message: string; category: string; status: 'pending' | 'in_progress' | 'resolved' | 'closed'; assigned_to: string | null; admin_notes: string | null; resolved_at: string | null; resolved_by: string | null; thread_id: string; last_admin_response_at: string | null; response_count: number; ticket_number: string; } interface EmailThread { id: string; created_at: string; message_id: string; from_email: string; to_email: string; subject: string; body_text: string; direction: 'inbound' | 'outbound'; sent_by: string | null; } export default function AdminContact() { const queryClient = useQueryClient(); const { theme } = useTheme(); const { isAdmin, loading: rolesLoading } = useUserRole(); const [statusFilter, setStatusFilter] = useState('all'); const [categoryFilter, setCategoryFilter] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [selectedSubmission, setSelectedSubmission] = useState(null); const [adminNotes, setAdminNotes] = useState(''); const [replyBody, setReplyBody] = useState(''); const [showReplyForm, setShowReplyForm] = useState(false); const [emailThreads, setEmailThreads] = useState([]); const [loadingThreads, setLoadingThreads] = useState(false); const [copiedTicket, setCopiedTicket] = useState(null); const [activeTab, setActiveTab] = useState('details'); const [replyStatus, setReplyStatus] = useState(''); // Fetch contact submissions const { data: submissions, isLoading } = useQuery({ queryKey: ['admin-contact-submissions', statusFilter, categoryFilter, searchQuery], queryFn: async () => { let query = supabase .from('contact_submissions') .select('*') .order('created_at', { ascending: false }); if (statusFilter !== 'all') { query = query.eq('status', statusFilter); } if (categoryFilter !== 'all') { query = query.eq('category', categoryFilter); } if (searchQuery) { query = query.or( `name.ilike.%${searchQuery}%,email.ilike.%${searchQuery}%,subject.ilike.%${searchQuery}%` ); } const { data, error } = await query; if (error) { logger.error('Failed to fetch contact submissions', { error: error.message }); throw error; } return data as ContactSubmission[]; }, }); // Fetch email threads when submission selected useEffect(() => { if (selectedSubmission) { setLoadingThreads(true); // Force fresh fetch - no caching supabase .from('contact_email_threads') .select('*') .eq('submission_id', selectedSubmission.id) .order('created_at', { ascending: true }) .then(({ data, error }) => { if (error) { logger.error('Failed to fetch email threads', { error }); setEmailThreads([]); } else { setEmailThreads((data as EmailThread[]) || []); } setLoadingThreads(false); }); setAdminNotes(selectedSubmission.admin_notes || ''); setReplyBody(''); setShowReplyForm(false); setReplyStatus(selectedSubmission.status); // Initialize reply status } else { // Reset tab to details when dialog closes setActiveTab('details'); } }, [selectedSubmission]); // Send reply mutation const sendReplyMutation = useMutation({ mutationFn: async ({ submissionId, body, newStatus }: { submissionId: string, body: string, newStatus?: string }) => { const { data, error } = await invokeWithTracking( 'send-admin-email-reply', { submissionId, replyBody: body }, undefined ); if (error) throw error; // Update status if changed if (newStatus && selectedSubmission && newStatus !== selectedSubmission.status) { const updateData: Record = { status: newStatus }; if (newStatus === 'resolved' || newStatus === 'closed') { const { data: { user } } = await supabase.auth.getUser(); updateData.resolved_at = new Date().toISOString(); updateData.resolved_by = user?.id; } const { error: statusError } = await supabase .from('contact_submissions') .update(updateData) .eq('id', submissionId); if (statusError) { logger.error('Failed to update status', { error: statusError }); throw statusError; } } return data; }, onSuccess: (_data, variables) => { const submission = submissions?.find(s => s.id === variables.submissionId); const ticketNumber = submission?.ticket_number || 'Unknown'; let message = `Your email response has been sent for ticket ${ticketNumber}`; if (variables.newStatus && selectedSubmission && variables.newStatus !== selectedSubmission.status) { message += ` and status updated to ${variables.newStatus.replace('_', ' ')}`; } handleSuccess('Reply Sent', message); setReplyBody(''); setShowReplyForm(false); // Refetch threads if (selectedSubmission) { supabase .from('contact_email_threads') .select('*') .eq('submission_id', selectedSubmission.id) .order('created_at', { ascending: true }) .then(({ data }) => setEmailThreads((data as EmailThread[]) || [])); } queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] }); }, onError: (error: Error) => { handleError(error, { action: 'Send Email Reply' }); } }); // Update submission status const updateStatusMutation = useMutation({ mutationFn: async ({ id, status, notes, }: { id: string; status: string; notes?: string; }) => { const updateData: Record = { status }; if (notes) { updateData.admin_notes = notes; } if (status === 'resolved' || status === 'closed') { const { data: { user } } = await supabase.auth.getUser(); updateData.resolved_at = new Date().toISOString(); updateData.resolved_by = user?.id; } const { error } = await supabase .from('contact_submissions') .update(updateData) .eq('id', id); if (error) throw error; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] }); handleSuccess('Status Updated', 'Contact submission status has been updated'); setSelectedSubmission(null); setAdminNotes(''); }, onError: (error) => { handleError(error, { action: 'update_contact_status' }); }, }); const handleUpdateStatus = (status: string) => { if (!selectedSubmission) return; updateStatusMutation.mutate({ id: selectedSubmission.id, status, notes: adminNotes || undefined, }); }; const getStatusBadge = (status: string) => { const variants: Record = { pending: 'default', in_progress: 'secondary', resolved: 'outline', closed: 'outline', }; return ( {status.replace('_', ' ').toUpperCase()} ); }; const getCategoryLabel = (category: string) => { const cat = contactCategories.find((c) => c.value === category); return cat?.label || category; }; const handleRefreshSubmissions = () => { queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] }); }; const handleCopyTicket = (ticketNumber: string) => { navigator.clipboard.writeText(ticketNumber); setCopiedTicket(ticketNumber); setTimeout(() => setCopiedTicket(null), 2000); }; const handleQuickReply = (e: React.MouseEvent, submission: ContactSubmission) => { e.stopPropagation(); setSelectedSubmission(submission); setAdminNotes(submission.admin_notes || ''); setActiveTab('thread'); setShowReplyForm(true); setReplyStatus(submission.status); }; const handleRefreshThreads = () => { if (!selectedSubmission) return; setLoadingThreads(true); supabase .from('contact_email_threads') .select('*') .eq('submission_id', selectedSubmission.id) .order('created_at', { ascending: true }) .then(({ data, error }) => { if (error) { logger.error('Failed to refresh email threads', { error }); handleError(error, { action: 'Refresh Email Threads' }); } else { setEmailThreads((data as EmailThread[]) || []); handleSuccess('Refreshed', 'Email thread updated'); } setLoadingThreads(false); }); }; // Show loading state while roles are being fetched if (rolesLoading) { return (

Loading...

); } // Admin-only access check (after loading complete) if (!isAdmin()) { return (

Access Denied

Email response features are only available to administrators.

); } // Theme-aware EmailThreadItem component function EmailThreadItem({ thread }: { thread: EmailThread }) { const isOutbound = thread.direction === 'outbound'; return (
{isOutbound ? ( ) : ( )} {isOutbound ? 'Admin Reply' : thread.from_email} {isOutbound && ( Sent )}
{format(new Date(thread.created_at), 'MMM d, yyyy h:mm a')}
{thread.body_text}
); } const stats = { pending: submissions?.filter((s) => s.status === 'pending').length || 0, inProgress: submissions?.filter((s) => s.status === 'in_progress').length || 0, resolved: submissions?.filter((s) => s.status === 'resolved').length || 0, total: submissions?.length || 0, }; return ( {/* Header */}

Contact Submissions

Manage and respond to user contact form submissions

{/* Stats Cards */}
Pending {stats.pending} In Progress {stats.inProgress} Resolved {stats.resolved} Total {stats.total}
{/* Filters */}
{/* Search */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Status Filter */}
{/* Category Filter */}
{/* Submissions List */} {isLoading ? (

Loading submissions...

) : submissions && submissions.length > 0 ? (
{submissions.map((submission) => ( { setSelectedSubmission(submission); setAdminNotes(submission.admin_notes || ''); }} >

{submission.subject}

{ e.stopPropagation(); handleCopyTicket(submission.ticket_number); }} > {copiedTicket === submission.ticket_number ? ( <> Copied! ) : ( <> {submission.ticket_number} )} {getStatusBadge(submission.status)} {getCategoryLabel(submission.category)} {submission.response_count > 0 && ( 📧 {submission.response_count} {submission.response_count === 1 ? 'reply' : 'replies'} )}
{submission.name} ({submission.email}) {format(new Date(submission.created_at), 'MMM d, yyyy h:mm a')} {submission.last_admin_response_at && ( Last reply {format(new Date(submission.last_admin_response_at), 'MMM d')} )}

{submission.message}

))}
) : (

No submissions found

)} {/* Submission Detail Dialog */} { if (!open) { setSelectedSubmission(null); setAdminNotes(''); } }} > {selectedSubmission && ( <> {selectedSubmission.subject} handleCopyTicket(selectedSubmission.ticket_number)} > {copiedTicket === selectedSubmission.ticket_number ? ( <> Copied! ) : ( <> {selectedSubmission.ticket_number} )} {selectedSubmission.response_count > 0 && ( 📧 {selectedSubmission.response_count} {selectedSubmission.response_count === 1 ? 'reply' : 'replies'} )} Submitted {format(new Date(selectedSubmission.created_at), 'PPpp')} Details Email Thread ({emailThreads.length}) {activeTab === 'thread' && ( )}
{/* Sender Info */}

{selectedSubmission.name}

{selectedSubmission.email}

{/* User Context Section */} {selectedSubmission.submitter_profile_data && (

Submitter Context

{selectedSubmission.submitter_profile_data.avatar_url && ( )} {selectedSubmission.submitter_username?.[0]?.toUpperCase() || 'U'}
@{selectedSubmission.submitter_username} {selectedSubmission.submitter_profile_data.display_name && ( ({selectedSubmission.submitter_profile_data.display_name}) )} {selectedSubmission.submitter_reputation} rep
{selectedSubmission.submitter_profile_data.member_since && (
Member since {format(new Date(selectedSubmission.submitter_profile_data.member_since), 'MMM d, yyyy')}
)} {selectedSubmission.submitter_profile_data.stats && (
{selectedSubmission.submitter_profile_data.stats.rides} rides • {selectedSubmission.submitter_profile_data.stats.coasters} coasters • {selectedSubmission.submitter_profile_data.stats.parks} parks • {selectedSubmission.submitter_profile_data.stats.reviews} reviews
)}
)} {/* Subject & Category */}
{getCategoryLabel(selectedSubmission.category)}
{/* Message */}

{selectedSubmission.message}

{/* Admin Notes */}