diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index ea53b02c..ff5dd5ef 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -441,6 +441,65 @@ export type Database = { }, ] } + contact_email_threads: { + Row: { + body_html: string | null + body_text: string + created_at: string + direction: string + from_email: string + id: string + in_reply_to: string | null + message_id: string + metadata: Json | null + reference_chain: string[] | null + sent_by: string | null + subject: string + submission_id: string + to_email: string + } + Insert: { + body_html?: string | null + body_text: string + created_at?: string + direction: string + from_email: string + id?: string + in_reply_to?: string | null + message_id: string + metadata?: Json | null + reference_chain?: string[] | null + sent_by?: string | null + subject: string + submission_id: string + to_email: string + } + Update: { + body_html?: string | null + body_text?: string + created_at?: string + direction?: string + from_email?: string + id?: string + in_reply_to?: string | null + message_id?: string + metadata?: Json | null + reference_chain?: string[] | null + sent_by?: string | null + subject?: string + submission_id?: string + to_email?: string + } + Relationships: [ + { + foreignKeyName: "contact_email_threads_submission_id_fkey" + columns: ["submission_id"] + isOneToOne: false + referencedRelation: "contact_submissions" + referencedColumns: ["id"] + }, + ] + } contact_rate_limits: { Row: { email: string @@ -471,12 +530,15 @@ export type Database = { email: string id: string ip_address_hash: string | null + last_admin_response_at: string | null message: string name: string resolved_at: string | null resolved_by: string | null + response_count: number | null status: string subject: string + thread_id: string updated_at: string user_agent: string | null user_id: string | null @@ -489,12 +551,15 @@ export type Database = { email: string id?: string ip_address_hash?: string | null + last_admin_response_at?: string | null message: string name: string resolved_at?: string | null resolved_by?: string | null + response_count?: number | null status?: string subject: string + thread_id: string updated_at?: string user_agent?: string | null user_id?: string | null @@ -507,12 +572,15 @@ export type Database = { email?: string id?: string ip_address_hash?: string | null + last_admin_response_at?: string | null message?: string name?: string resolved_at?: string | null resolved_by?: string | null + response_count?: number | null status?: string subject?: string + thread_id?: string updated_at?: string user_agent?: string | null user_id?: string | null diff --git a/src/pages/admin/AdminContact.tsx b/src/pages/admin/AdminContact.tsx index 1ff3fbc9..cb7fa351 100644 --- a/src/pages/admin/AdminContact.tsx +++ b/src/pages/admin/AdminContact.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { @@ -10,6 +10,10 @@ import { Search, ChevronDown, AlertCircle, + Send, + ArrowUpRight, + ArrowDownLeft, + Loader2, } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import { Button } from '@/components/ui/button'; @@ -38,9 +42,14 @@ import { 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 { 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'; interface ContactSubmission { id: string; @@ -57,15 +66,53 @@ interface ContactSubmission { admin_notes: string | null; resolved_at: string | null; resolved_by: string | null; + thread_id: string; + last_admin_response_at: string | null; + response_count: number; +} + +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 } = 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); + + // Admin-only access check + if (!isAdmin()) { + return ( +
+ + + +

Access Denied

+

+ Email response features are only available to administrators. +

+
+
+
+ ); + } // Fetch contact submissions const { data: submissions, isLoading } = useQuery({ @@ -101,6 +148,62 @@ export default function AdminContact() { }, }); + // Fetch email threads when submission selected + useEffect(() => { + if (selectedSubmission) { + 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 fetch email threads', { error }); + setEmailThreads([]); + } else { + setEmailThreads((data as EmailThread[]) || []); + } + setLoadingThreads(false); + }); + + setAdminNotes(selectedSubmission.admin_notes || ''); + setReplyBody(''); + setShowReplyForm(false); + } + }, [selectedSubmission]); + + // Send reply mutation + const sendReplyMutation = useMutation({ + mutationFn: async ({ submissionId, body }: { submissionId: string, body: string }) => { + const { data, error } = await invokeWithTracking( + 'send-admin-email-reply', + { submissionId, replyBody: body }, + undefined + ); + if (error) throw error; + return data; + }, + onSuccess: () => { + handleSuccess('Reply Sent', 'Your email response has been sent successfully'); + 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 ({ @@ -172,6 +275,46 @@ export default function AdminContact() { return cat?.label || category; }; + // 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, @@ -296,6 +439,11 @@ export default function AdminContact() {

{submission.subject}

{getStatusBadge(submission.status)} {getCategoryLabel(submission.category)} + {submission.response_count > 0 && ( + + 📧 {submission.response_count} {submission.response_count === 1 ? 'reply' : 'replies'} + + )}
@@ -306,6 +454,12 @@ export default function AdminContact() { {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}

@@ -336,118 +490,187 @@ export default function AdminContact() { } }} > - + {selectedSubmission && ( <> - + + {selectedSubmission.subject} + {selectedSubmission.response_count > 0 && ( + + 📧 {selectedSubmission.response_count} {selectedSubmission.response_count === 1 ? 'reply' : 'replies'} + + )} Submitted {format(new Date(selectedSubmission.created_at), 'PPpp')} -
- {/* Sender Info */} -
- -
- {selectedSubmission.name} - - ({selectedSubmission.email}) - -
-
+ + + + Details + + + Email Thread ({emailThreads.length}) + + - {/* Category & Status */} -
-
- -
{getCategoryLabel(selectedSubmission.category)}
-
-
- -
{getStatusBadge(selectedSubmission.status)}
-
-
+ +
+ {/* Sender Info */} +
+
+ +

{selectedSubmission.name}

+
+
+ +

{selectedSubmission.email}

+
+
- {/* Message */} -
- -
- {selectedSubmission.message} -
-
+ {/* Subject & Category */} +
+ +
+ {getCategoryLabel(selectedSubmission.category)} +
+
- {/* Existing Admin Notes */} - {selectedSubmission.admin_notes && ( -
- -
- {selectedSubmission.admin_notes} + {/* Message */} +
+ + +

{selectedSubmission.message}

+
+
+ + {/* Admin Notes */} +
+ +