From 1da0bc04d66933d8d91e54705d3e0b7c218fcb37 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:48:03 +0000 Subject: [PATCH] feat: Implement ticket merging --- src/integrations/supabase/types.ts | 3 + src/pages/admin/AdminContact.tsx | 262 +++++++++++++++++- supabase/config.toml | 3 + .../functions/merge-contact-tickets/index.ts | 239 ++++++++++++++++ ...2_bf97dab5-01c6-47f2-91dd-1afa9762236e.sql | 9 + 5 files changed, 501 insertions(+), 15 deletions(-) create mode 100644 supabase/functions/merge-contact-tickets/index.ts create mode 100644 supabase/migrations/20251028224512_bf97dab5-01c6-47f2-91dd-1afa9762236e.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index f0110c9b..6631f394 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -540,6 +540,7 @@ export type Database = { id: string ip_address_hash: string | null last_admin_response_at: string | null + merged_ticket_numbers: string[] | null message: string name: string resolved_at: string | null @@ -567,6 +568,7 @@ export type Database = { id?: string ip_address_hash?: string | null last_admin_response_at?: string | null + merged_ticket_numbers?: string[] | null message: string name: string resolved_at?: string | null @@ -594,6 +596,7 @@ export type Database = { id?: string ip_address_hash?: string | null last_admin_response_at?: string | null + merged_ticket_numbers?: string[] | null message?: string name?: string resolved_at?: string | null diff --git a/src/pages/admin/AdminContact.tsx b/src/pages/admin/AdminContact.tsx index 64b601cd..555da808 100644 --- a/src/pages/admin/AdminContact.tsx +++ b/src/pages/admin/AdminContact.tsx @@ -25,6 +25,7 @@ import { ArchiveRestore, Trash2, AlertTriangle, + Merge, } from 'lucide-react'; import { supabase } from '@/integrations/supabase/client'; import { Button } from '@/components/ui/button'; @@ -67,6 +68,10 @@ 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 { Checkbox } from '@/components/ui/checkbox'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { DialogFooter } from '@/components/ui/dialog'; import { useTheme } from '@/components/theme/ThemeProvider'; import { useUserRole } from '@/hooks/useUserRole'; import { handleError, handleSuccess } from '@/lib/errorHandler'; @@ -110,6 +115,7 @@ interface ContactSubmission { ticket_number: string; archived_at: string | null; archived_by: string | null; + merged_ticket_numbers: string[] | null; } interface EmailThread { @@ -141,6 +147,13 @@ export default function AdminContact() { const [activeTab, setActiveTab] = useState('details'); const [replyStatus, setReplyStatus] = useState(''); const [showArchived, setShowArchived] = useState(false); + + // Merge mode state + const [selectedForMerge, setSelectedForMerge] = useState([]); + const [mergeMode, setMergeMode] = useState(false); + const [showMergeDialog, setShowMergeDialog] = useState(false); + const [primaryTicketId, setPrimaryTicketId] = useState(null); + const [mergeReason, setMergeReason] = useState(''); // Fetch contact submissions const { data: submissions, isLoading } = useQuery({ @@ -451,6 +464,57 @@ export default function AdminContact() { }); }; + // Merge tickets mutation + const mergeTicketsMutation = useMutation({ + mutationFn: async ({ primaryId, mergeIds, reason }: { + primaryId: string; + mergeIds: string[]; + reason?: string; + }) => { + const { data, error } = await invokeWithTracking( + 'merge-contact-tickets', + { + primaryTicketId: primaryId, + mergeTicketIds: mergeIds, + mergeReason: reason + }, + undefined + ); + if (error) throw error; + return data; + }, + onSuccess: (data: any) => { + handleSuccess( + 'Tickets Merged Successfully', + `Merged ${data.mergedCount} tickets into ${data.primaryTicketNumber}. ${data.threadsConsolidated} email threads consolidated.` + ); + + // Reset merge mode + setMergeMode(false); + setSelectedForMerge([]); + setShowMergeDialog(false); + setPrimaryTicketId(null); + setMergeReason(''); + + // Refresh data + queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] }); + }, + onError: (error: Error) => { + handleError(error, { action: 'Merge Tickets' }); + } + }); + + const handleConfirmMerge = () => { + if (!primaryTicketId || selectedForMerge.length < 2) return; + + const mergeIds = selectedForMerge.filter(id => id !== primaryTicketId); + mergeTicketsMutation.mutate({ + primaryId: primaryTicketId, + mergeIds, + reason: mergeReason || undefined + }); + }; + // Show loading state while roles are being fetched if (rolesLoading) { return ( @@ -536,6 +600,17 @@ export default function AdminContact() {

+ - -
+ {!mergeMode && ( +
+ + +
+ )} @@ -741,6 +841,113 @@ export default function AdminContact() { )} + {/* Floating Merge Button */} + {mergeMode && selectedForMerge.length >= 2 && ( +
+ +
+ )} + + {/* Merge Confirmation Dialog */} + + + + Merge Contact Tickets + + Choose a primary ticket to keep. All others will be permanently deleted after their data is merged. + + + + {/* List selected tickets with radio buttons */} +
+ {selectedForMerge.map(ticketId => { + const ticket = submissions?.find(s => s.id === ticketId); + if (!ticket) return null; + + return ( +
+ setPrimaryTicketId(ticketId)} + className="mt-1" + /> +
+
{ticket.ticket_number}
+
{ticket.subject}
+
+ {ticket.email} • {ticket.response_count} email threads +
+
+
+ ); + })} +
+ + {/* Optional merge reason */} +
+ +