mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 16:51:13 -05:00
feat: Implement ticket system and reply button
This commit is contained in:
@@ -539,6 +539,7 @@ export type Database = {
|
|||||||
status: string
|
status: string
|
||||||
subject: string
|
subject: string
|
||||||
thread_id: string | null
|
thread_id: string | null
|
||||||
|
ticket_number: string | null
|
||||||
updated_at: string
|
updated_at: string
|
||||||
user_agent: string | null
|
user_agent: string | null
|
||||||
user_id: string | null
|
user_id: string | null
|
||||||
@@ -560,6 +561,7 @@ export type Database = {
|
|||||||
status?: string
|
status?: string
|
||||||
subject: string
|
subject: string
|
||||||
thread_id?: string | null
|
thread_id?: string | null
|
||||||
|
ticket_number?: string | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
user_agent?: string | null
|
user_agent?: string | null
|
||||||
user_id?: string | null
|
user_id?: string | null
|
||||||
@@ -581,6 +583,7 @@ export type Database = {
|
|||||||
status?: string
|
status?: string
|
||||||
subject?: string
|
subject?: string
|
||||||
thread_id?: string | null
|
thread_id?: string | null
|
||||||
|
ticket_number?: string | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
user_agent?: string | null
|
user_agent?: string | null
|
||||||
user_id?: string | null
|
user_id?: string | null
|
||||||
@@ -3954,6 +3957,7 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
extract_cf_image_id: { Args: { url: string }; Returns: string }
|
extract_cf_image_id: { Args: { url: string }; Returns: string }
|
||||||
generate_deletion_confirmation_code: { Args: never; Returns: string }
|
generate_deletion_confirmation_code: { Args: never; Returns: string }
|
||||||
|
generate_ticket_number: { Args: never; Returns: string }
|
||||||
get_email_change_status: { Args: never; Returns: Json }
|
get_email_change_status: { Args: never; Returns: Json }
|
||||||
get_filtered_profile: {
|
get_filtered_profile: {
|
||||||
Args: { _profile_user_id: string; _viewer_id?: string }
|
Args: { _profile_user_id: string; _viewer_id?: string }
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import {
|
|||||||
ArrowDownLeft,
|
ArrowDownLeft,
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Reply,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -71,6 +74,7 @@ interface ContactSubmission {
|
|||||||
thread_id: string;
|
thread_id: string;
|
||||||
last_admin_response_at: string | null;
|
last_admin_response_at: string | null;
|
||||||
response_count: number;
|
response_count: number;
|
||||||
|
ticket_number: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmailThread {
|
interface EmailThread {
|
||||||
@@ -98,6 +102,7 @@ export default function AdminContact() {
|
|||||||
const [showReplyForm, setShowReplyForm] = useState(false);
|
const [showReplyForm, setShowReplyForm] = useState(false);
|
||||||
const [emailThreads, setEmailThreads] = useState<EmailThread[]>([]);
|
const [emailThreads, setEmailThreads] = useState<EmailThread[]>([]);
|
||||||
const [loadingThreads, setLoadingThreads] = useState(false);
|
const [loadingThreads, setLoadingThreads] = useState(false);
|
||||||
|
const [copiedTicket, setCopiedTicket] = useState<string | null>(null);
|
||||||
|
|
||||||
// Fetch contact submissions
|
// Fetch contact submissions
|
||||||
const { data: submissions, isLoading } = useQuery({
|
const { data: submissions, isLoading } = useQuery({
|
||||||
@@ -169,8 +174,10 @@ export default function AdminContact() {
|
|||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (_data, variables) => {
|
||||||
handleSuccess('Reply Sent', 'Your email response has been sent successfully');
|
const submission = submissions?.find(s => s.id === variables.submissionId);
|
||||||
|
const ticketNumber = submission?.ticket_number || 'Unknown';
|
||||||
|
handleSuccess('Reply Sent', `Your email response has been sent for ticket ${ticketNumber}`);
|
||||||
setReplyBody('');
|
setReplyBody('');
|
||||||
setShowReplyForm(false);
|
setShowReplyForm(false);
|
||||||
// Refetch threads
|
// Refetch threads
|
||||||
@@ -264,6 +271,19 @@ export default function AdminContact() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ['admin-contact-submissions'] });
|
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 || '');
|
||||||
|
setShowReplyForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
// Show loading state while roles are being fetched
|
// Show loading state while roles are being fetched
|
||||||
if (rolesLoading) {
|
if (rolesLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -463,6 +483,20 @@ export default function AdminContact() {
|
|||||||
<div className="flex-1 space-y-2">
|
<div className="flex-1 space-y-2">
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<h3 className="font-semibold text-lg">{submission.subject}</h3>
|
<h3 className="font-semibold text-lg">{submission.subject}</h3>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono cursor-pointer hover:bg-accent transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCopyTicket(submission.ticket_number);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{copiedTicket === submission.ticket_number ? (
|
||||||
|
<><Check className="h-3 w-3 mr-1" /> Copied!</>
|
||||||
|
) : (
|
||||||
|
<><Copy className="h-3 w-3 mr-1" /> {submission.ticket_number}</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
{getStatusBadge(submission.status)}
|
{getStatusBadge(submission.status)}
|
||||||
<Badge variant="outline">{getCategoryLabel(submission.category)}</Badge>
|
<Badge variant="outline">{getCategoryLabel(submission.category)}</Badge>
|
||||||
{submission.response_count > 0 && (
|
{submission.response_count > 0 && (
|
||||||
@@ -489,8 +523,20 @@ export default function AdminContact() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm line-clamp-2">{submission.message}</p>
|
<p className="text-sm line-clamp-2">{submission.message}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => handleQuickReply(e, submission)}
|
||||||
|
disabled={sendReplyMutation.isPending}
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<Reply className="h-4 w-4" />
|
||||||
|
Reply
|
||||||
|
</Button>
|
||||||
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -520,11 +566,22 @@ export default function AdminContact() {
|
|||||||
{selectedSubmission && (
|
{selectedSubmission && (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2 flex-wrap">
|
||||||
<Mail className="h-5 w-5" />
|
<Mail className="h-5 w-5" />
|
||||||
{selectedSubmission.subject}
|
{selectedSubmission.subject}
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono cursor-pointer hover:bg-accent transition-colors"
|
||||||
|
onClick={() => handleCopyTicket(selectedSubmission.ticket_number)}
|
||||||
|
>
|
||||||
|
{copiedTicket === selectedSubmission.ticket_number ? (
|
||||||
|
<><Check className="h-3 w-3 mr-1" /> Copied!</>
|
||||||
|
) : (
|
||||||
|
<><Copy className="h-3 w-3 mr-1" /> {selectedSubmission.ticket_number}</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
{selectedSubmission.response_count > 0 && (
|
{selectedSubmission.response_count > 0 && (
|
||||||
<Badge variant="secondary" className="ml-2 text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
📧 {selectedSubmission.response_count} {selectedSubmission.response_count === 1 ? 'reply' : 'replies'}
|
📧 {selectedSubmission.response_count} {selectedSubmission.response_count === 1 ? 'reply' : 'replies'}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -654,9 +711,17 @@ export default function AdminContact() {
|
|||||||
) : (
|
) : (
|
||||||
<Card className="border-primary/20">
|
<Card className="border-primary/20">
|
||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
<Label className="text-sm font-medium mb-2 block">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
Reply to {selectedSubmission.name} ({selectedSubmission.email})
|
Reply to {selectedSubmission.name} ({selectedSubmission.email})
|
||||||
</Label>
|
</Label>
|
||||||
|
<Badge variant="outline" className="font-mono">
|
||||||
|
{selectedSubmission.ticket_number}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mb-3">
|
||||||
|
Subject: Re: [{selectedSubmission.ticket_number}] {selectedSubmission.subject}
|
||||||
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
value={replyBody}
|
value={replyBody}
|
||||||
onChange={(e) => setReplyBody(e.target.value)}
|
onChange={(e) => setReplyBody(e.target.value)}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
// Fetch submission
|
// Fetch submission
|
||||||
const { data: submission, error: fetchError } = await supabase
|
const { data: submission, error: fetchError } = await supabase
|
||||||
.from('contact_submissions')
|
.from('contact_submissions')
|
||||||
.select('id, email, name, subject, thread_id, response_count')
|
.select('id, email, name, subject, thread_id, response_count, ticket_number')
|
||||||
.eq('id', submissionId)
|
.eq('id', submissionId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
@@ -89,8 +89,9 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
}, 429, corsHeaders);
|
}, 429, corsHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageId = `<${crypto.randomUUID()}@thrillwiki.com>`;
|
const ticketNumber = submission.ticket_number || 'UNKNOWN';
|
||||||
const finalSubject = replySubject || `Re: ${submission.subject}`;
|
const messageId = `<${ticketNumber}.${crypto.randomUUID()}@thrillwiki.com>`;
|
||||||
|
const finalSubject = replySubject || `Re: [${ticketNumber}] ${submission.subject}`;
|
||||||
|
|
||||||
// Get previous message for threading
|
// Get previous message for threading
|
||||||
const { data: previousMessages } = await supabase
|
const { data: previousMessages } = await supabase
|
||||||
@@ -100,7 +101,13 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const inReplyTo = previousMessages?.[0]?.message_id || `<${submission.thread_id}@thrillwiki.com>`;
|
const originalMessageId = `<${ticketNumber}.${submission.id}@thrillwiki.com>`;
|
||||||
|
const inReplyTo = previousMessages?.[0]?.message_id || originalMessageId;
|
||||||
|
|
||||||
|
// Build reference chain for threading
|
||||||
|
const referenceChain = previousMessages?.[0]?.message_id
|
||||||
|
? [originalMessageId, previousMessages[0].message_id].join(' ')
|
||||||
|
: originalMessageId;
|
||||||
|
|
||||||
// Send email via ForwardEmail
|
// Send email via ForwardEmail
|
||||||
const forwardEmailResponse = await fetch('https://api.forwardemail.net/v1/emails', {
|
const forwardEmailResponse = await fetch('https://api.forwardemail.net/v1/emails', {
|
||||||
@@ -117,8 +124,9 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
headers: {
|
headers: {
|
||||||
'Message-ID': messageId,
|
'Message-ID': messageId,
|
||||||
'In-Reply-To': inReplyTo,
|
'In-Reply-To': inReplyTo,
|
||||||
'References': inReplyTo,
|
'References': referenceChain,
|
||||||
'X-Thread-ID': submission.thread_id
|
'X-Thread-ID': submission.thread_id,
|
||||||
|
'X-Ticket-Number': ticketNumber
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
userId = user?.id || null;
|
userId = user?.id || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert contact submission
|
// Insert contact submission (ticket number auto-generated by trigger)
|
||||||
const { data: submission, error: insertError } = await supabase
|
const { data: submission, error: insertError } = await supabase
|
||||||
.from('contact_submissions')
|
.from('contact_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -153,7 +153,7 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
ip_address_hash: ipHash,
|
ip_address_hash: ipHash,
|
||||||
status: 'pending'
|
status: 'pending'
|
||||||
})
|
})
|
||||||
.select()
|
.select('*, ticket_number')
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (insertError) {
|
if (insertError) {
|
||||||
@@ -174,6 +174,9 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS') || 'noreply@thrillwiki.com';
|
const fromEmail = Deno.env.get('FROM_EMAIL_ADDRESS') || 'noreply@thrillwiki.com';
|
||||||
const forwardEmailKey = Deno.env.get('FORWARDEMAIL_API_KEY');
|
const forwardEmailKey = Deno.env.get('FORWARDEMAIL_API_KEY');
|
||||||
|
|
||||||
|
const ticketNumber = submission.ticket_number || 'PENDING';
|
||||||
|
const messageId = `<${ticketNumber}.${submission.id}@thrillwiki.com>`;
|
||||||
|
|
||||||
if (forwardEmailKey) {
|
if (forwardEmailKey) {
|
||||||
// Send admin notification
|
// Send admin notification
|
||||||
fetch('https://api.forwardemail.net/v1/emails', {
|
fetch('https://api.forwardemail.net/v1/emails', {
|
||||||
@@ -185,9 +188,10 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
from: fromEmail,
|
from: fromEmail,
|
||||||
to: adminEmail,
|
to: adminEmail,
|
||||||
subject: `New Contact Form Submission - ${category.charAt(0).toUpperCase() + category.slice(1)}`,
|
subject: `[${ticketNumber}] New Contact - ${category.charAt(0).toUpperCase() + category.slice(1)}`,
|
||||||
text: `A new contact message has been received:
|
text: `A new contact message has been received:
|
||||||
|
|
||||||
|
Ticket: ${ticketNumber}
|
||||||
From: ${name} (${email})
|
From: ${name} (${email})
|
||||||
Category: ${category.charAt(0).toUpperCase() + category.slice(1)}
|
Category: ${category.charAt(0).toUpperCase() + category.slice(1)}
|
||||||
Subject: ${subject}
|
Subject: ${subject}
|
||||||
@@ -199,6 +203,10 @@ Reference ID: ${submission.id}
|
|||||||
Submitted: ${new Date(submission.created_at).toLocaleString()}
|
Submitted: ${new Date(submission.created_at).toLocaleString()}
|
||||||
|
|
||||||
View in admin panel: https://thrillwiki.com/admin/contact`,
|
View in admin panel: https://thrillwiki.com/admin/contact`,
|
||||||
|
headers: {
|
||||||
|
'Message-ID': messageId,
|
||||||
|
'X-Ticket-Number': ticketNumber
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
edgeLogger.error('Failed to send admin notification', { requestId, error: err.message });
|
edgeLogger.error('Failed to send admin notification', { requestId, error: err.message });
|
||||||
@@ -214,25 +222,37 @@ View in admin panel: https://thrillwiki.com/admin/contact`,
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
from: fromEmail,
|
from: fromEmail,
|
||||||
to: email,
|
to: email,
|
||||||
subject: "We've received your message - ThrillWiki Support",
|
subject: `[${ticketNumber}] We've received your message - ThrillWiki Support`,
|
||||||
text: `Hi ${name},
|
text: `Hi ${name},
|
||||||
|
|
||||||
Thank you for contacting ThrillWiki! We've received your message and will respond within 24-48 hours.
|
Thank you for contacting ThrillWiki! We've received your message and will respond within 24-48 hours.
|
||||||
|
|
||||||
Your Message Details:
|
Your Message Details:
|
||||||
|
Ticket Number: ${ticketNumber}
|
||||||
Category: ${category.charAt(0).toUpperCase() + category.slice(1)}
|
Category: ${category.charAt(0).toUpperCase() + category.slice(1)}
|
||||||
Subject: ${subject}
|
Subject: ${subject}
|
||||||
|
|
||||||
Reference ID: ${submission.id}
|
When replying to this email, please keep the ticket number in the subject line to ensure your response is properly tracked.
|
||||||
|
|
||||||
Our support team will review your message and get back to you as soon as possible.
|
Our support team will review your message and get back to you as soon as possible.
|
||||||
|
|
||||||
Best regards,
|
Best regards,
|
||||||
The ThrillWiki Team`,
|
The ThrillWiki Team`,
|
||||||
|
headers: {
|
||||||
|
'Message-ID': messageId,
|
||||||
|
'X-Ticket-Number': ticketNumber,
|
||||||
|
'References': messageId
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
edgeLogger.error('Failed to send confirmation email', { requestId, error: err.message });
|
edgeLogger.error('Failed to send confirmation email', { requestId, error: err.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update thread_id with ticket number
|
||||||
|
await supabase
|
||||||
|
.from('contact_submissions')
|
||||||
|
.update({ thread_id: `ticket-${ticketNumber}` })
|
||||||
|
.eq('id', submission.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
@@ -246,7 +266,8 @@ The ThrillWiki Team`,
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
success: true,
|
success: true,
|
||||||
submissionId: submission.id,
|
submissionId: submission.id,
|
||||||
message: 'Your message has been received. We will respond within 24-48 hours.'
|
ticketNumber: ticketNumber,
|
||||||
|
message: `Your message has been received (Ticket: ${ticketNumber}). We will respond within 24-48 hours.`
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
-- Add ticket number system to contact submissions
|
||||||
|
-- Create sequence for ticket numbers starting at 100000
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS contact_ticket_number_seq START 100000;
|
||||||
|
|
||||||
|
-- Add ticket_number column with unique constraint
|
||||||
|
ALTER TABLE public.contact_submissions
|
||||||
|
ADD COLUMN IF NOT EXISTS ticket_number TEXT UNIQUE;
|
||||||
|
|
||||||
|
-- Create function to generate ticket number in format TW-XXXXXX
|
||||||
|
CREATE OR REPLACE FUNCTION generate_ticket_number()
|
||||||
|
RETURNS TEXT AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN 'TW-' || LPAD(nextval('contact_ticket_number_seq')::TEXT, 6, '0');
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- Create function to auto-set ticket number on insert
|
||||||
|
CREATE OR REPLACE FUNCTION set_ticket_number()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.ticket_number IS NULL THEN
|
||||||
|
NEW.ticket_number := generate_ticket_number();
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- Add trigger to auto-generate ticket numbers
|
||||||
|
DROP TRIGGER IF EXISTS set_ticket_number_trigger ON public.contact_submissions;
|
||||||
|
CREATE TRIGGER set_ticket_number_trigger
|
||||||
|
BEFORE INSERT ON public.contact_submissions
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_ticket_number();
|
||||||
|
|
||||||
|
-- Backfill existing submissions with ticket numbers based on creation order
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
rec RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR rec IN
|
||||||
|
SELECT id
|
||||||
|
FROM public.contact_submissions
|
||||||
|
WHERE ticket_number IS NULL
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
LOOP
|
||||||
|
UPDATE public.contact_submissions
|
||||||
|
SET ticket_number = generate_ticket_number()
|
||||||
|
WHERE id = rec.id;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Add index for ticket_number lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contact_submissions_ticket_number
|
||||||
|
ON public.contact_submissions(ticket_number);
|
||||||
Reference in New Issue
Block a user