feat: Implement ticket system and reply button

This commit is contained in:
gpt-engineer-app[bot]
2025-10-28 18:33:32 +00:00
parent 4d21dc4435
commit ab21dc9c82
5 changed files with 172 additions and 20 deletions

View File

@@ -15,6 +15,9 @@ import {
ArrowDownLeft,
Loader2,
RefreshCw,
Reply,
Copy,
Check,
} from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { Button } from '@/components/ui/button';
@@ -71,6 +74,7 @@ interface ContactSubmission {
thread_id: string;
last_admin_response_at: string | null;
response_count: number;
ticket_number: string;
}
interface EmailThread {
@@ -98,6 +102,7 @@ export default function AdminContact() {
const [showReplyForm, setShowReplyForm] = useState(false);
const [emailThreads, setEmailThreads] = useState<EmailThread[]>([]);
const [loadingThreads, setLoadingThreads] = useState(false);
const [copiedTicket, setCopiedTicket] = useState<string | null>(null);
// Fetch contact submissions
const { data: submissions, isLoading } = useQuery({
@@ -169,8 +174,10 @@ export default function AdminContact() {
if (error) throw error;
return data;
},
onSuccess: () => {
handleSuccess('Reply Sent', 'Your email response has been sent successfully');
onSuccess: (_data, variables) => {
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('');
setShowReplyForm(false);
// Refetch threads
@@ -264,6 +271,19 @@ export default function AdminContact() {
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
if (rolesLoading) {
return (
@@ -463,6 +483,20 @@ export default function AdminContact() {
<div className="flex-1 space-y-2">
<div className="flex items-center gap-3 flex-wrap">
<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)}
<Badge variant="outline">{getCategoryLabel(submission.category)}</Badge>
{submission.response_count > 0 && (
@@ -489,7 +523,19 @@ export default function AdminContact() {
</div>
<p className="text-sm line-clamp-2">{submission.message}</p>
</div>
<ChevronDown className="h-5 w-5 text-muted-foreground" />
<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" />
</div>
</div>
</CardContent>
</Card>
@@ -520,11 +566,22 @@ export default function AdminContact() {
{selectedSubmission && (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<DialogTitle className="flex items-center gap-2 flex-wrap">
<Mail className="h-5 w-5" />
{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 && (
<Badge variant="secondary" className="ml-2 text-xs">
<Badge variant="secondary" className="text-xs">
📧 {selectedSubmission.response_count} {selectedSubmission.response_count === 1 ? 'reply' : 'replies'}
</Badge>
)}
@@ -654,9 +711,17 @@ export default function AdminContact() {
) : (
<Card className="border-primary/20">
<CardContent className="pt-4">
<Label className="text-sm font-medium mb-2 block">
Reply to {selectedSubmission.name} ({selectedSubmission.email})
</Label>
<div className="mb-3 flex items-center justify-between">
<Label className="text-sm font-medium">
Reply to {selectedSubmission.name} ({selectedSubmission.email})
</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
value={replyBody}
onChange={(e) => setReplyBody(e.target.value)}