"use client"; import { useState, useEffect, useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Badge } from "~/components/ui/badge"; import { Separator } from "~/components/ui/separator"; import { Alert, AlertDescription } from "~/components/ui/alert"; import { Label } from "~/components/ui/label"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { PageHeader } from "~/components/layout/page-header"; import { FloatingActionBar } from "~/components/layout/floating-action-bar"; import { EmailComposer } from "~/components/forms/email-composer"; import { EmailPreview } from "~/components/forms/email-preview"; import { api } from "~/trpc/react"; import { toast } from "sonner"; import { Mail, Send, Eye, Edit3, AlertTriangle, ArrowLeft, Loader2, FileText, } from "lucide-react"; function SendEmailPageSkeleton() { return (
); } export default function SendEmailPage() { const params = useParams(); const router = useRouter(); const invoiceId = params.id as string; // State management const [activeTab, setActiveTab] = useState("compose"); const [isSending, setIsSending] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [retryCount, setRetryCount] = useState(0); // Email content state const [subject, setSubject] = useState(""); const [emailContent, setEmailContent] = useState(""); const [ccEmail, setCcEmail] = useState(""); const [bccEmail, setBccEmail] = useState(""); const [customMessage, setCustomMessage] = useState(""); // Fetch invoice data const { data: invoiceData, isLoading: invoiceLoading } = api.invoices.getById.useQuery({ id: invoiceId, }); // Get utils for cache invalidation const utils = api.useUtils(); // Email sending mutation const sendEmailMutation = api.email.sendInvoice.useMutation({ onSuccess: (data) => { toast.success("Email sent successfully!", { description: data.message, duration: 5000, }); // Navigate back to invoice view router.push(`/dashboard/invoices/${invoiceId}`); // Refresh invoice data void utils.invoices.getById.invalidate({ id: invoiceId }); }, onError: (error) => { let errorMessage = "Failed to send invoice email"; let errorDescription = error.message; let canRetry = false; if (error.message.includes("Invalid recipient")) { errorMessage = "Invalid Email Address"; errorDescription = "Please check the client's email address and try again."; } else if (error.message.includes("domain not verified")) { errorMessage = "Email Configuration Issue"; errorDescription = "Please contact support to configure email sending."; } else if (error.message.includes("rate limit")) { errorMessage = "Too Many Emails"; errorDescription = "Please wait a moment before sending another email."; canRetry = true; } else if (error.message.includes("no email address")) { errorMessage = "No Email Address"; errorDescription = "This client doesn't have an email address on file."; } else if ( error.message.includes("unavailable") || error.message.includes("timeout") ) { errorMessage = "Service Temporarily Unavailable"; errorDescription = "The email service is temporarily unavailable. Please try again."; canRetry = true; } else { canRetry = true; // Allow retry for unknown errors } toast.error(errorMessage, { description: canRetry && retryCount < 2 ? `${errorDescription} You can retry this operation.` : errorDescription, duration: 6000, action: canRetry && retryCount < 2 ? { label: "Retry", onClick: () => handleRetry(), } : undefined, }); setIsSending(false); }, }); // Transform invoice data for components const invoice = useMemo(() => { return invoiceData ? { id: invoiceData.id, invoiceNumber: invoiceData.invoiceNumber, issueDate: invoiceData.issueDate, dueDate: invoiceData.dueDate, status: invoiceData.status, taxRate: invoiceData.taxRate, client: invoiceData.client ? { name: invoiceData.client.name, email: invoiceData.client.email, } : undefined, business: invoiceData.business ? { name: invoiceData.business.name, nickname: invoiceData.business.nickname, email: invoiceData.business.email, } : undefined, items: invoiceData.items?.map((item) => ({ id: item.id, hours: item.hours, rate: item.rate, })), } : undefined; }, [invoiceData]); // Initialize email content when invoice loads useEffect(() => { if (!invoice || isInitialized) return; // Set default subject const defaultSubject = `Invoice ${invoice.invoiceNumber} from ${invoice.business?.name ?? "Your Business"}`; // eslint-disable-next-line react-hooks/set-state-in-effect setSubject(defaultSubject); // Set default content (empty since template handles everything) const defaultContent = ``; setEmailContent(defaultContent); setIsInitialized(true); }, [invoice, isInitialized]); const handleSendEmail = async () => { if (!invoice?.client?.email || invoice.client.email.trim() === "") { toast.error("No email address", { description: "This client doesn't have an email address on file.", }); return; } if (!subject.trim()) { toast.error("Subject required", { description: "Please enter an email subject before sending.", }); return; } // Show confirmation dialog setShowConfirmDialog(true); }; const confirmSendEmail = async () => { setShowConfirmDialog(false); setIsSending(true); try { await sendEmailMutation.mutateAsync({ invoiceId, customSubject: subject, customContent: emailContent, customMessage: customMessage?.trim() || undefined, useHtml: true, ccEmails: ccEmail.trim() || undefined, bccEmails: bccEmail.trim() || undefined, }); setRetryCount(0); // Reset retry count on success } catch { // Error handling is done in the mutation's onError } }; const handleRetry = () => { if (retryCount < 2) { setRetryCount((prev) => prev + 1); void confirmSendEmail(); } }; const fromEmail = invoice?.business?.email ?? "noreply@yourdomain.com"; const toEmail = invoice?.client?.email ?? ""; const canSend = !isSending && subject.trim() && toEmail && toEmail.trim() !== ""; if (invoiceLoading) { return ; } if (!invoice) { return (
Invoice not found.
); } return (
{/* Warning for missing email */} {(!toEmail || toEmail.trim() === "") && ( This client doesn't have an email address. Please add an email address to the client before sending the invoice. )} {/* Main Content */}
Compose Preview
Compose Email {isInitialized ? ( ) : (

Initializing email content...

)}
Email Preview
{/* Sidebar */}
{/* Invoice Summary */} Invoice #{invoice.invoiceNumber}

{invoice.client?.name ?? "Client"}

{new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric", }).format(new Date(invoice.issueDate))}

{invoice.status}
Email Details

{fromEmail}

{toEmail || "No email address"}

{ccEmail && (

{ccEmail}

)} {bccEmail && (

{bccEmail}

)}

{subject || "No subject"}

invoice-{invoice.invoiceNumber}.pdf
Actions {activeTab === "compose" && ( )} {activeTab === "preview" && ( )}
{/* Floating Action Bar */}

Send Invoice

Email invoice to {invoice.client?.name ?? "client"}

} > {/* Confirmation Dialog */} Send Invoice Email? This will send invoice #{invoice.invoiceNumber} to{" "} {invoice.client?.email} {ccEmail && ( <> {" "} with CC to {ccEmail} )} {bccEmail && ( <> {" "} and BCC to {bccEmail} )} . {retryCount > 0 && (
Retry attempt {retryCount} of 2
)}
); }