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 */}
+
);
}
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}
` : ""}
-
@@ -497,7 +484,7 @@ export function generateInvoiceEmailTemplate({