diff --git a/src/app/dashboard/invoices/[id]/send/page.tsx b/src/app/dashboard/invoices/[id]/send/page.tsx index af496a9..8747774 100644 --- a/src/app/dashboard/invoices/[id]/send/page.tsx +++ b/src/app/dashboard/invoices/[id]/send/page.tsx @@ -9,6 +9,14 @@ 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"; @@ -55,6 +63,8 @@ export default function SendEmailPage() { 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(""); @@ -87,10 +97,9 @@ export default function SendEmailPage() { void utils.invoices.getById.invalidate({ id: invoiceId }); }, onError: (error) => { - console.error("Email send error:", 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"; @@ -102,14 +111,35 @@ export default function SendEmailPage() { } 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: errorDescription, + description: + canRetry && retryCount < 2 + ? `${errorDescription} You can retry this operation.` + : errorDescription, duration: 6000, + action: + canRetry && retryCount < 2 + ? { + label: "Retry", + onClick: () => handleRetry(), + } + : undefined, }); setIsSending(false); @@ -177,8 +207,12 @@ export default function SendEmailPage() { return; } - // Email content is now optional since template handles default messaging + // Show confirmation dialog + setShowConfirmDialog(true); + }; + const confirmSendEmail = async () => { + setShowConfirmDialog(false); setIsSending(true); try { @@ -191,9 +225,16 @@ export default function SendEmailPage() { ccEmails: ccEmail.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 - 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() { + + {/* 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 +
+ )} +
+
+ + + + +
+
); } diff --git a/src/components/forms/enhanced-send-invoice-button.tsx b/src/components/forms/enhanced-send-invoice-button.tsx index 33df443..fe3448c 100644 --- a/src/components/forms/enhanced-send-invoice-button.tsx +++ b/src/components/forms/enhanced-send-invoice-button.tsx @@ -49,8 +49,8 @@ export function EnhancedSendInvoiceButton({ !hasClientEmail ? "Client has no email address" : showResend - ? "Resend Invoice" - : "Send Invoice" + ? "Resend Email" + : "Compose Email" } > {invoiceLoading ? ( @@ -90,7 +90,7 @@ export function EnhancedSendInvoiceButton({ ) : ( )} - {showResend ? "Resend Invoice" : "Send Invoice"} + {showResend ? "Resend Email" : "Compose Email"} )} diff --git a/src/components/layout/floating-action-bar.tsx b/src/components/layout/floating-action-bar.tsx index 24918ec..70a410c 100644 --- a/src/components/layout/floating-action-bar.tsx +++ b/src/components/layout/floating-action-bar.tsx @@ -61,8 +61,8 @@ export function FloatingActionBar({ {/* Content container - full width when floating, content width when docked */}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 6b479e9..001fc04 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "~/lib/utils"; 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: { variant: { @@ -16,7 +16,7 @@ const buttonVariants = cva( 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", 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: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", ghost: diff --git a/src/lib/email-templates/invoice-email.ts b/src/lib/email-templates/invoice-email.ts index 0e8bd31..e3c5255 100644 --- a/src/lib/email-templates/invoice-email.ts +++ b/src/lib/email-templates/invoice-email.ts @@ -43,7 +43,7 @@ export function generateInvoiceEmailTemplate({ customMessage, userName, userEmail, - baseUrl = "https://beenvoice.app", + baseUrl: _baseUrl = "https://beenvoice.app", }: InvoiceEmailTemplateProps): { html: string; text: string } { const formatDate = (date: Date) => { return new Intl.DateTimeFormat("en-US", { @@ -173,44 +173,25 @@ export function generateInvoiceEmailTemplate({ margin: 24px 0; } - .invoice-header { - display: flex; - justify-content: space-between; - align-items: flex-start; + .invoice-summary { margin-bottom: 20px; - flex-wrap: wrap; - gap: 16px; } .invoice-number { font-size: 24px; font-weight: bold; color: #16a34a; - margin-bottom: 4px; + margin-bottom: 8px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; } .invoice-date { font-size: 14px; color: #6b7280; - } - - .invoice-amount { - text-align: right; - } - - .amount-label { - font-size: 14px; - color: #6b7280; 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 { border-top: 1px solid #e5e7eb; @@ -219,12 +200,14 @@ export function generateInvoiceEmailTemplate({ } .detail-row { - display: table; + border-collapse: separate; + border-spacing: 0; width: 100%; - padding: 8px 0; border-bottom: 1px solid #f3f4f6; } + + .detail-row:last-child { border-bottom: none; font-weight: 600; @@ -234,21 +217,19 @@ export function generateInvoiceEmailTemplate({ } .detail-label { - display: table-cell; font-size: 14px; color: #6b7280; text-align: left; - width: 50%; + padding: 8px 0; } .detail-value { - display: table-cell; font-size: 14px; color: #1f2937; font-weight: bold; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; text-align: right; - width: 50%; + padding: 8px 0; } .business-info { @@ -349,11 +330,14 @@ export function generateInvoiceEmailTemplate({ border-top: 1px solid #e5e7eb; } - .footer-logo { - max-width: 80px; - height: auto; + .footer-brand { + font-size: 18px; + font-weight: bold; + color: #16a34a; margin: 0 auto 8px; display: block; + text-align: center; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; } .footer-text { @@ -378,6 +362,12 @@ export function generateInvoiceEmailTemplate({ mso-table-rspace: 0pt; } + /* Gmail specific fixes */ + .gmail-fix { + border-collapse: separate !important; + border-spacing: 0 !important; + } + /* Apple Mail attachment preview fix */ .attachment-notice { border: 2px dashed #bbf7d0 !important; @@ -404,15 +394,10 @@ export function generateInvoiceEmailTemplate({ text-align: left; } - .detail-row { - display: block; - } - - .detail-label, - .detail-value { - display: block; - width: 100%; - text-align: left; + .detail-row td { + display: block !important; + width: 100% !important; + text-align: left !important; } } @@ -434,39 +419,41 @@ export function generateInvoiceEmailTemplate({ ${customContent ? `
${customContent}
` : ""}
-
-
-
#${invoice.invoiceNumber}
-
Issue Date: ${formatDate(invoice.issueDate)}
-
Due Date: ${formatDate(invoice.dueDate)}
-
-
-
Total Amount
-
${formatCurrency(total)}
-
+
+
#${invoice.invoiceNumber}
+
Issue Date: ${formatDate(invoice.issueDate)}
+
Due Date: ${formatDate(invoice.dueDate)}
-
- Client - ${invoice.client.name} -
-
- Subtotal - ${formatCurrency(subtotal)} -
+ + + + + +
Client${invoice.client.name}
+ + + + + +
Subtotal${formatCurrency(subtotal)}
${ invoice.taxRate > 0 - ? `
- Tax (${invoice.taxRate}%) - ${formatCurrency(taxAmount)} -
` + ? ` + + + + +
Tax (${invoice.taxRate}%)${formatCurrency(taxAmount)}
` : "" } -
- Total - ${formatCurrency(total)} -
+ + + + + +
Total${formatCurrency(total)}
@@ -497,7 +484,7 @@ export function generateInvoiceEmailTemplate({