mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 17:48:55 -04:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user