refactor: remove InvoiceView component and update related email and invoice handling

- Deleted the InvoiceView component to streamline the codebase.
- Updated EmailPreview and SendEmailDialog components to include currency and notes fields.
- Enhanced invoice-form to handle default hourly rates and improved item mapping.
- Refactored email template generation to include notes and currency formatting.
- Adjusted API routers for invoices to calculate totals and handle notes and currency correctly.
This commit is contained in:
2026-04-28 00:34:56 -04:00
parent ad89ad001d
commit 84a5d997b4
12 changed files with 739 additions and 969 deletions
@@ -1,127 +0,0 @@
"use client";
import { useState } from "react";
import { Button } from "~/components/ui/button";
import { toast } from "sonner";
import { api } from "~/trpc/react";
import { Send, Loader2 } from "lucide-react";
interface SendInvoiceButtonProps {
invoiceId: string;
variant?: "default" | "outline" | "ghost" | "icon";
className?: string;
showResend?: boolean;
}
export function SendInvoiceButton({
invoiceId,
variant = "outline",
className,
showResend = false,
}: SendInvoiceButtonProps) {
const [isSending, setIsSending] = useState(false);
// Get utils for cache invalidation
const utils = api.useUtils();
// Use the new email API mutation
const sendInvoiceMutation = api.email.sendInvoice.useMutation({
onSuccess: (data) => {
// Show detailed success message with delivery info
toast.success(data.message, {
description: `Email ID: ${data.emailId}`,
duration: 5000,
});
// Refresh invoice data to show updated status
void utils.invoices.getById.invalidate({ id: invoiceId });
},
onError: (error) => {
// Enhanced error handling with specific error types
console.error("Email send error:", error);
let errorMessage = "Failed to send invoice email";
let errorDescription = "";
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.";
} else if (error.message.includes("no email address")) {
errorMessage = "No Email Address";
errorDescription = "This client doesn't have an email address on file.";
} else {
errorDescription = error.message;
}
toast.error(errorMessage, {
description: errorDescription,
duration: 6000,
});
},
});
const handleSendInvoice = async () => {
if (isSending) return;
setIsSending(true);
try {
await sendInvoiceMutation.mutateAsync({
invoiceId,
});
} catch (error) {
// Error is already handled by the mutation's onError
console.error("Send invoice error:", error);
} finally {
setIsSending(false);
}
};
if (variant === "icon") {
return (
<Button
onClick={handleSendInvoice}
disabled={isSending}
variant="ghost"
size="sm"
className={className}
>
{isSending ? (
<Loader2 className="h-3 w-3 animate-spin sm:h-4 sm:w-4" />
) : (
<Send className="h-3 w-3 sm:h-4 sm:w-4" />
)}
</Button>
);
}
return (
<Button
onClick={handleSendInvoice}
disabled={isSending}
variant={variant}
size="default"
className={`w-full shadow-sm ${className}`}
data-testid="send-invoice-button"
>
{isSending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Sending Email...</span>
</>
) : (
<>
<Send className="mr-2 h-4 w-4" />
<span>{showResend ? "Resend Invoice" : "Send Invoice"}</span>
</>
)}
</Button>
);
}
@@ -1,26 +0,0 @@
"use client";
import { InvoiceView } from "~/components/data/invoice-view";
import InvoiceForm from "~/components/forms/invoice-form";
interface UnifiedInvoicePageProps {
invoiceId: string;
mode: string;
}
export function UnifiedInvoicePage({
invoiceId,
mode,
}: UnifiedInvoicePageProps) {
return (
<div>
{/* Always render InvoiceForm to preserve state, but hide when in view mode */}
<div className={mode === "edit" ? "block" : "hidden"}>
<InvoiceForm invoiceId={invoiceId} />
</div>
{/* Show InvoiceView only when in view mode */}
{mode === "view" && <InvoiceView invoiceId={invoiceId} />}
</div>
);
}
+2 -2
View File
@@ -99,10 +99,10 @@ function InvoiceViewContent({ invoiceId }: { invoiceId: string }) {
}).format(new Date(date));
};
const formatCurrency = (amount: number) => {
const formatCurrency = (amount: number, currency = invoice.currency) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
currency,
}).format(amount);
};
+34 -28
View File
@@ -136,9 +136,9 @@ export default function SendEmailPage() {
action:
canRetry && retryCount < 2
? {
label: "Retry",
onClick: () => handleRetry(),
}
label: "Retry",
onClick: () => handleRetry(),
}
: undefined,
});
@@ -150,31 +150,37 @@ export default function SendEmailPage() {
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,
})),
}
id: invoiceData.id,
invoiceNumber: invoiceData.invoiceNumber,
issueDate: invoiceData.issueDate,
dueDate: invoiceData.dueDate,
status: invoiceData.status,
totalAmount: invoiceData.totalAmount,
taxRate: invoiceData.taxRate,
currency: invoiceData.currency,
notes: invoiceData.notes,
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,
date: item.date,
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.amount,
})),
}
: undefined;
}, [invoiceData]);