Add confirmation dialog before sending invoice email

The commit adds a confirmation dialog when sending invoices, improves
error handling with retries, and refines email-related UI text.
This commit is contained in:
2025-07-29 20:15:40 -04:00
parent 8cd9035f3c
commit acc8731e09
7 changed files with 159 additions and 132 deletions

View File

@@ -9,6 +9,14 @@ import { Badge } from "~/components/ui/badge";
import { Separator } from "~/components/ui/separator"; import { Separator } from "~/components/ui/separator";
import { Alert, AlertDescription } from "~/components/ui/alert"; import { Alert, AlertDescription } from "~/components/ui/alert";
import { Label } from "~/components/ui/label"; 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 { PageHeader } from "~/components/layout/page-header";
import { FloatingActionBar } from "~/components/layout/floating-action-bar"; import { FloatingActionBar } from "~/components/layout/floating-action-bar";
import { EmailComposer } from "~/components/forms/email-composer"; import { EmailComposer } from "~/components/forms/email-composer";
@@ -55,6 +63,8 @@ export default function SendEmailPage() {
const [activeTab, setActiveTab] = useState("compose"); const [activeTab, setActiveTab] = useState("compose");
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const [retryCount, setRetryCount] = useState(0);
// Email content state // Email content state
const [subject, setSubject] = useState(""); const [subject, setSubject] = useState("");
@@ -87,10 +97,9 @@ export default function SendEmailPage() {
void utils.invoices.getById.invalidate({ id: invoiceId }); void utils.invoices.getById.invalidate({ id: invoiceId });
}, },
onError: (error) => { onError: (error) => {
console.error("Email send error:", error);
let errorMessage = "Failed to send invoice email"; let errorMessage = "Failed to send invoice email";
let errorDescription = error.message; let errorDescription = error.message;
let canRetry = false;
if (error.message.includes("Invalid recipient")) { if (error.message.includes("Invalid recipient")) {
errorMessage = "Invalid Email Address"; errorMessage = "Invalid Email Address";
@@ -102,14 +111,35 @@ export default function SendEmailPage() {
} else if (error.message.includes("rate limit")) { } else if (error.message.includes("rate limit")) {
errorMessage = "Too Many Emails"; errorMessage = "Too Many Emails";
errorDescription = "Please wait a moment before sending another email."; errorDescription = "Please wait a moment before sending another email.";
canRetry = true;
} else if (error.message.includes("no email address")) { } else if (error.message.includes("no email address")) {
errorMessage = "No Email Address"; errorMessage = "No Email Address";
errorDescription = "This client doesn't have an email address on file."; 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, { toast.error(errorMessage, {
description: errorDescription, description:
canRetry && retryCount < 2
? `${errorDescription} You can retry this operation.`
: errorDescription,
duration: 6000, duration: 6000,
action:
canRetry && retryCount < 2
? {
label: "Retry",
onClick: () => handleRetry(),
}
: undefined,
}); });
setIsSending(false); setIsSending(false);
@@ -177,8 +207,12 @@ export default function SendEmailPage() {
return; return;
} }
// Email content is now optional since template handles default messaging // Show confirmation dialog
setShowConfirmDialog(true);
};
const confirmSendEmail = async () => {
setShowConfirmDialog(false);
setIsSending(true); setIsSending(true);
try { try {
@@ -191,9 +225,16 @@ export default function SendEmailPage() {
ccEmails: ccEmail.trim() || undefined, ccEmails: ccEmail.trim() || undefined,
bccEmails: bccEmail.trim() || undefined, bccEmails: bccEmail.trim() || undefined,
}); });
} catch (error) { setRetryCount(0); // Reset retry count on success
} catch {
// Error handling is done in the mutation's onError // Error handling is done in the mutation's onError
console.error("Send email error:", error); }
};
const handleRetry = () => {
if (retryCount < 2) {
setRetryCount((prev) => prev + 1);
void confirmSendEmail();
} }
}; };
@@ -490,7 +531,7 @@ export default function SendEmailPage() {
<Button <Button
onClick={handleSendEmail} onClick={handleSendEmail}
disabled={!canSend || isSending} disabled={!canSend || isSending}
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg" className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-colors duration-200 hover:from-emerald-700 hover:to-teal-700"
size="sm" size="sm"
> >
{isSending ? ( {isSending ? (
@@ -506,6 +547,52 @@ export default function SendEmailPage() {
)} )}
</Button> </Button>
</FloatingActionBar> </FloatingActionBar>
{/* Confirmation Dialog */}
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Send Invoice Email?</DialogTitle>
<DialogDescription>
This will send invoice #{invoice.invoiceNumber} to{" "}
<strong>{invoice.client?.email}</strong>
{ccEmail && (
<>
{" "}
with CC to <strong>{ccEmail}</strong>
</>
)}
{bccEmail && (
<>
{" "}
and BCC to <strong>{bccEmail}</strong>
</>
)}
.
{retryCount > 0 && (
<div className="mt-2 text-sm text-yellow-600">
Retry attempt {retryCount} of 2
</div>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowConfirmDialog(false)}
>
Cancel
</Button>
<Button
onClick={confirmSendEmail}
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700"
>
<Send className="mr-2 h-4 w-4" />
Send Email
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
); );
} }

View File

@@ -49,8 +49,8 @@ export function EnhancedSendInvoiceButton({
!hasClientEmail !hasClientEmail
? "Client has no email address" ? "Client has no email address"
: showResend : showResend
? "Resend Invoice" ? "Resend Email"
: "Send Invoice" : "Compose Email"
} }
> >
{invoiceLoading ? ( {invoiceLoading ? (
@@ -90,7 +90,7 @@ export function EnhancedSendInvoiceButton({
) : ( ) : (
<Send className="mr-2 h-4 w-4" /> <Send className="mr-2 h-4 w-4" />
)} )}
<span>{showResend ? "Resend Invoice" : "Send Invoice"}</span> <span>{showResend ? "Resend Email" : "Compose Email"}</span>
</> </>
)} )}
</Button> </Button>

View File

@@ -61,8 +61,8 @@ export function FloatingActionBar({
{/* Content container - full width when floating, content width when docked */} {/* Content container - full width when floating, content width when docked */}
<div <div
className={cn( className={cn(
"w-full transition-all duration-300", "w-full transition-transform duration-300",
isDocked ? "mx-auto px-4 mb-0" : "px-4 mb-4", isDocked ? "mx-auto mb-0 px-4" : "mb-4 px-4",
)} )}
> >
<Card className="card-primary"> <Card className="card-primary">

View File

@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
@@ -16,7 +16,7 @@ const buttonVariants = cva(
destructive: destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline: outline:
"border border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-accent-foreground hover:border-border/60 transition-all duration-200", "border border-border/40 bg-background/60 shadow-sm backdrop-blur-sm hover:bg-accent/50 hover:text-accent-foreground hover:border-border/60 transition-colors duration-150",
secondary: secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: ghost:

View File

@@ -43,7 +43,7 @@ export function generateInvoiceEmailTemplate({
customMessage, customMessage,
userName, userName,
userEmail, userEmail,
baseUrl = "https://beenvoice.app", baseUrl: _baseUrl = "https://beenvoice.app",
}: InvoiceEmailTemplateProps): { html: string; text: string } { }: InvoiceEmailTemplateProps): { html: string; text: string } {
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
return new Intl.DateTimeFormat("en-US", { return new Intl.DateTimeFormat("en-US", {
@@ -173,44 +173,25 @@ export function generateInvoiceEmailTemplate({
margin: 24px 0; margin: 24px 0;
} }
.invoice-header { .invoice-summary {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px; margin-bottom: 20px;
flex-wrap: wrap;
gap: 16px;
} }
.invoice-number { .invoice-number {
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
color: #16a34a; color: #16a34a;
margin-bottom: 4px; margin-bottom: 8px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
} }
.invoice-date { .invoice-date {
font-size: 14px; font-size: 14px;
color: #6b7280; color: #6b7280;
}
.invoice-amount {
text-align: right;
}
.amount-label {
font-size: 14px;
color: #6b7280;
margin-bottom: 4px; margin-bottom: 4px;
} }
.amount-value {
font-size: 28px;
font-weight: bold;
color: #1f2937;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
}
.invoice-details { .invoice-details {
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
@@ -219,12 +200,14 @@ export function generateInvoiceEmailTemplate({
} }
.detail-row { .detail-row {
display: table; border-collapse: separate;
border-spacing: 0;
width: 100%; width: 100%;
padding: 8px 0;
border-bottom: 1px solid #f3f4f6; border-bottom: 1px solid #f3f4f6;
} }
.detail-row:last-child { .detail-row:last-child {
border-bottom: none; border-bottom: none;
font-weight: 600; font-weight: 600;
@@ -234,21 +217,19 @@ export function generateInvoiceEmailTemplate({
} }
.detail-label { .detail-label {
display: table-cell;
font-size: 14px; font-size: 14px;
color: #6b7280; color: #6b7280;
text-align: left; text-align: left;
width: 50%; padding: 8px 0;
} }
.detail-value { .detail-value {
display: table-cell;
font-size: 14px; font-size: 14px;
color: #1f2937; color: #1f2937;
font-weight: bold; font-weight: bold;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
text-align: right; text-align: right;
width: 50%; padding: 8px 0;
} }
.business-info { .business-info {
@@ -349,11 +330,14 @@ export function generateInvoiceEmailTemplate({
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
} }
.footer-logo { .footer-brand {
max-width: 80px; font-size: 18px;
height: auto; font-weight: bold;
color: #16a34a;
margin: 0 auto 8px; margin: 0 auto 8px;
display: block; display: block;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
} }
.footer-text { .footer-text {
@@ -378,6 +362,12 @@ export function generateInvoiceEmailTemplate({
mso-table-rspace: 0pt; mso-table-rspace: 0pt;
} }
/* Gmail specific fixes */
.gmail-fix {
border-collapse: separate !important;
border-spacing: 0 !important;
}
/* Apple Mail attachment preview fix */ /* Apple Mail attachment preview fix */
.attachment-notice { .attachment-notice {
border: 2px dashed #bbf7d0 !important; border: 2px dashed #bbf7d0 !important;
@@ -404,15 +394,10 @@ export function generateInvoiceEmailTemplate({
text-align: left; text-align: left;
} }
.detail-row { .detail-row td {
display: block; display: block !important;
} width: 100% !important;
text-align: left !important;
.detail-label,
.detail-value {
display: block;
width: 100%;
text-align: left;
} }
} }
</style> </style>
@@ -434,39 +419,41 @@ export function generateInvoiceEmailTemplate({
${customContent ? `<div class="message custom-content">${customContent}</div>` : ""} ${customContent ? `<div class="message custom-content">${customContent}</div>` : ""}
<div class="invoice-card"> <div class="invoice-card">
<div class="invoice-header"> <div class="invoice-summary">
<div> <div class="invoice-number">#${invoice.invoiceNumber}</div>
<div class="invoice-number">#${invoice.invoiceNumber}</div> <div class="invoice-date">Issue Date: ${formatDate(invoice.issueDate)}</div>
<div class="invoice-date">Issue Date: ${formatDate(invoice.issueDate)}</div> <div class="invoice-date">Due Date: ${formatDate(invoice.dueDate)}</div>
<div class="invoice-date">Due Date: ${formatDate(invoice.dueDate)}</div>
</div>
<div class="invoice-amount">
<div class="amount-label">Total Amount</div>
<div class="amount-value">${formatCurrency(total)}</div>
</div>
</div> </div>
<div class="invoice-details"> <div class="invoice-details">
<div class="detail-row"> <table class="detail-row" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-collapse: separate; border-spacing: 0; width: 100%; border-bottom: 1px solid #f3f4f6;">
<span class="detail-label">Client</span> <tr>
<span class="detail-value">${invoice.client.name}</span> <td class="detail-label" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: left;">Client</td>
</div> <td class="detail-value" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: right;">${invoice.client.name}</td>
<div class="detail-row"> </tr>
<span class="detail-label">Subtotal</span> </table>
<span class="detail-value">${formatCurrency(subtotal)}</span> <table class="detail-row" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-collapse: separate; border-spacing: 0; width: 100%; border-bottom: 1px solid #f3f4f6;">
</div> <tr>
<td class="detail-label" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: left;">Subtotal</td>
<td class="detail-value" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: right;">${formatCurrency(subtotal)}</td>
</tr>
</table>
${ ${
invoice.taxRate > 0 invoice.taxRate > 0
? `<div class="detail-row"> ? `<table class="detail-row" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-collapse: separate; border-spacing: 0; width: 100%; border-bottom: 1px solid #f3f4f6;">
<span class="detail-label">Tax (${invoice.taxRate}%)</span> <tr>
<span class="detail-value">${formatCurrency(taxAmount)}</span> <td class="detail-label" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: left;">Tax (${invoice.taxRate}%)</td>
</div>` <td class="detail-value" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: right;">${formatCurrency(taxAmount)}</td>
</tr>
</table>`
: "" : ""
} }
<div class="detail-row"> <table class="detail-row" cellpadding="0" cellspacing="0" border="0" width="100%" style="border-collapse: separate; border-spacing: 0; width: 100%; border-top: 2px solid #e5e7eb; margin-top: 8px; padding-top: 12px;">
<span class="detail-label">Total</span> <tr>
<span class="detail-value">${formatCurrency(total)}</span> <td class="detail-label" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: left; font-weight: bold; font-size: 16px;">Total</td>
</div> <td class="detail-value" style="width: 50%; vertical-align: top; padding: 8px 0; text-align: right; font-weight: bold; font-size: 18px; color: #16a34a;">${formatCurrency(total)}</td>
</tr>
</table>
</div> </div>
</div> </div>
@@ -497,7 +484,7 @@ export function generateInvoiceEmailTemplate({
</div> </div>
<div class="footer"> <div class="footer">
<img src="${baseUrl}/beenvoice-logo.svg" alt="beenvoice" class="footer-logo" /> <div class="footer-brand">beenvoice</div>
${ ${
invoice.business invoice.business
? `<div class="footer-text"> ? `<div class="footer-text">

View File

@@ -65,8 +65,7 @@ const registerFonts = () => {
} }
fontsRegistered = true; fontsRegistered = true;
} catch (error) { } catch {
console.warn("Font registration failed, using built-in fonts:", error);
fontsRegistered = true; // Don't keep trying fontsRegistered = true; // Don't keep trying
} }
}; };
@@ -1076,8 +1075,6 @@ export async function generateInvoicePDF(invoice: InvoiceData): Promise<void> {
// Download the PDF // Download the PDF
saveAs(blob, filename); saveAs(blob, filename);
} catch (error) { } catch (error) {
console.error("PDF generation error:", error);
// Provide more specific error messages // Provide more specific error messages
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes("timeout")) { if (error.message.includes("timeout")) {
@@ -1102,8 +1099,6 @@ export async function generateInvoicePDF(invoice: InvoiceData): Promise<void> {
export async function generateInvoicePDFBlob( export async function generateInvoicePDFBlob(
invoice: InvoiceData, invoice: InvoiceData,
): Promise<Blob> { ): Promise<Blob> {
const isServerSide = typeof window === "undefined";
try { try {
// Ensure fonts are registered (important for server-side generation) // Ensure fonts are registered (important for server-side generation)
registerFonts(); registerFonts();
@@ -1121,10 +1116,6 @@ export async function generateInvoicePDFBlob(
throw new Error("Client information is required"); throw new Error("Client information is required");
} }
console.log(
`Generating PDF blob for invoice ${invoice.invoiceNumber} (${isServerSide ? "server-side" : "client-side"})...`,
);
// Generate PDF blob with timeout (same as generateInvoicePDF) // Generate PDF blob with timeout (same as generateInvoicePDF)
const pdfPromise = pdf(<InvoicePDF invoice={invoice} />).toBlob(); const pdfPromise = pdf(<InvoicePDF invoice={invoice} />).toBlob();
const timeoutPromise = new Promise<never>((_, reject) => const timeoutPromise = new Promise<never>((_, reject) =>
@@ -1137,17 +1128,8 @@ export async function generateInvoicePDFBlob(
if (!blob || blob.size === 0) { if (!blob || blob.size === 0) {
throw new Error("Generated PDF is empty"); throw new Error("Generated PDF is empty");
} }
console.log(
`PDF blob generated successfully, size: ${blob.size} bytes (${isServerSide ? "server-side" : "client-side"})`,
);
return blob; return blob;
} catch (error) { } catch (error) {
console.error(
`PDF generation error for email attachment (${isServerSide ? "server-side" : "client-side"}):`,
error,
);
// Provide more specific error messages (same as generateInvoicePDF) // Provide more specific error messages (same as generateInvoicePDF)
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes("timeout")) { if (error.message.includes("timeout")) {

View File

@@ -119,15 +119,6 @@ export const emailRouter = createTRPCRouter({
let resendInstance: Resend; let resendInstance: Resend;
let fromEmail: string; let fromEmail: string;
console.log("Email configuration debug:");
console.log(
"- Business resendApiKey:",
invoice.business?.resendApiKey ? "***SET***" : "not set",
);
console.log("- Business resendDomain:", invoice.business?.resendDomain);
console.log("- System RESEND_DOMAIN:", env.RESEND_DOMAIN);
console.log("- Business email:", invoice.business?.email);
// Check if business has custom Resend configuration // Check if business has custom Resend configuration
if (invoice.business?.resendApiKey && invoice.business?.resendDomain) { if (invoice.business?.resendApiKey && invoice.business?.resendDomain) {
// Use business's custom Resend setup // Use business's custom Resend setup
@@ -135,21 +126,16 @@ export const emailRouter = createTRPCRouter({
const fromName = const fromName =
invoice.business.emailFromName ?? invoice.business.name ?? userName; invoice.business.emailFromName ?? invoice.business.name ?? userName;
fromEmail = `${fromName} <noreply@${invoice.business.resendDomain}>`; fromEmail = `${fromName} <noreply@${invoice.business.resendDomain}>`;
console.log("- Using business custom Resend configuration");
} else if (env.RESEND_DOMAIN) { } else if (env.RESEND_DOMAIN) {
// Use system Resend configuration // Use system Resend configuration
resendInstance = defaultResend; resendInstance = defaultResend;
fromEmail = `noreply@${env.RESEND_DOMAIN}`; fromEmail = `noreply@${env.RESEND_DOMAIN}`;
console.log("- Using system Resend configuration");
} else { } else {
// Fallback to business email if no configured domains // Fallback to business email if no configured domains
resendInstance = defaultResend; resendInstance = defaultResend;
fromEmail = invoice.business?.email ?? "noreply@yourdomain.com"; fromEmail = invoice.business?.email ?? "noreply@yourdomain.com";
console.log("- Using fallback configuration");
} }
console.log("- Final fromEmail:", fromEmail);
// Prepare CC and BCC lists // Prepare CC and BCC lists
const ccEmails: string[] = []; const ccEmails: string[] = [];
const bccEmails: string[] = []; const bccEmails: string[] = [];
@@ -163,8 +149,6 @@ export const emailRouter = createTRPCRouter({
for (const email of ccList) { for (const email of ccList) {
if (emailRegex.test(email)) { if (emailRegex.test(email)) {
ccEmails.push(email); ccEmails.push(email);
} else {
console.warn("Invalid CC email format, skipping:", email);
} }
} }
} }
@@ -178,8 +162,6 @@ export const emailRouter = createTRPCRouter({
for (const email of bccList) { for (const email of bccList) {
if (emailRegex.test(email)) { if (emailRegex.test(email)) {
bccEmails.push(email); bccEmails.push(email);
} else {
console.warn("Invalid BCC email format, skipping:", email);
} }
} }
} }
@@ -189,11 +171,6 @@ export const emailRouter = createTRPCRouter({
// Validate business email format before adding to CC // Validate business email format before adding to CC
if (emailRegex.test(invoice.business.email)) { if (emailRegex.test(invoice.business.email)) {
ccEmails.push(invoice.business.email); ccEmails.push(invoice.business.email);
} else {
console.warn(
"Invalid business email format, skipping CC:",
invoice.business.email,
);
} }
} }
@@ -222,8 +199,7 @@ export const emailRouter = createTRPCRouter({
}, },
], ],
}); });
} catch (sendError) { } catch {
console.error("Resend API call failed:", sendError);
throw new Error( throw new Error(
"Email service is currently unavailable. Please try again later.", "Email service is currently unavailable. Please try again later.",
); );
@@ -231,7 +207,6 @@ export const emailRouter = createTRPCRouter({
// Enhanced error checking // Enhanced error checking
if (emailResult.error) { if (emailResult.error) {
console.error("Resend API error:", emailResult.error);
const errorMsg = emailResult.error.message?.toLowerCase() ?? ""; const errorMsg = emailResult.error.message?.toLowerCase() ?? "";
// Provide more specific error messages based on error type // Provide more specific error messages based on error type
@@ -287,12 +262,8 @@ export const emailRouter = createTRPCRouter({
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(invoices.id, input.invoiceId)); .where(eq(invoices.id, input.invoiceId));
} catch (dbError) { } catch {
console.error("Failed to update invoice status:", dbError);
// Don't throw here - email was sent successfully, status update is secondary // Don't throw here - email was sent successfully, status update is secondary
console.warn(
`Invoice ${invoice.invoiceNumber} sent but status not updated`,
);
} }
} }