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 738 additions and 968 deletions
+12 -6
View File
@@ -17,6 +17,8 @@ interface EmailPreviewProps {
taxRate: number;
status?: string;
totalAmount?: number;
currency?: string | null;
notes?: string | null;
client?: {
name: string;
email: string | null;
@@ -27,8 +29,11 @@ interface EmailPreviewProps {
};
items?: Array<{
id: string;
date?: Date;
description?: string;
hours: number;
rate: number;
amount?: number;
}>;
};
className?: string;
@@ -66,7 +71,8 @@ export function EmailPreview({
status: invoice.status ?? "draft",
totalAmount: invoice.totalAmount ?? calculateTotal(),
taxRate: invoice.taxRate,
notes: null,
currency: invoice.currency,
notes: invoice.notes,
client: {
name: invoice.client?.name ?? "Client",
email: invoice.client?.email ?? null,
@@ -74,11 +80,11 @@ export function EmailPreview({
business: invoice.business ?? null,
items:
invoice.items?.map((item) => ({
date: new Date(),
description: "Service",
date: item.date ?? new Date(),
description: item.description ?? "Service",
hours: item.hours,
rate: item.rate,
amount: item.hours * item.rate,
amount: item.amount ?? item.hours * item.rate,
})) ?? [],
},
customContent: content,
@@ -95,7 +101,7 @@ export function EmailPreview({
return (
<div className={className}>
{/* Email Headers */}
<div className="bg-muted/20 mb-4 space-y-3 p-4">
<div className="bg-muted/20 mb-4 space-y-3 p-4">
<div className="grid grid-cols-1 gap-3 text-sm md:grid-cols-3">
<div>
<span className="text-muted-foreground block text-xs font-medium">
@@ -142,7 +148,7 @@ export function EmailPreview({
{/* Email Content */}
{emailTemplate ? (
<div className=" border bg-gray-50 p-1 shadow-sm">
<div className="border bg-gray-50 p-1 shadow-sm">
<iframe
srcDoc={emailTemplate.html}
className="h-[700px] w-full rounded border-0"
+31 -36
View File
@@ -76,6 +76,13 @@ function InvoiceFormSkeleton() {
);
}
function getDefaultHourlyRate(value: unknown) {
if (typeof value !== "object" || value === null) return null;
const rate = (value as { defaultHourlyRate?: unknown }).defaultHourlyRate;
return typeof rate === "number" ? rate : null;
}
export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const router = useRouter();
const utils = api.useUtils();
@@ -140,18 +147,14 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
if (invoiceId && invoiceId !== "new" && existingInvoice && !initialized) {
// ... (Mapping logic same as before)
const mappedItems: InvoiceItem[] =
existingInvoice.items
?.map((item) => ({
id: crypto.randomUUID(),
date: new Date(item.date),
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.amount,
}))
.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
) || [];
existingInvoice.items?.map((item) => ({
id: crypto.randomUUID(),
date: new Date(item.date),
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.amount,
})) || [];
setFormData({
invoiceNumber: existingInvoice.invoiceNumber,
invoicePrefix: existingInvoice.invoicePrefix ?? "#",
@@ -341,17 +344,13 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
notes: formData.notes,
taxRate: formData.taxRate,
currency: formData.currency,
items: formData.items
.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
)
.map((i) => ({
date: i.date,
description: i.description,
hours: i.hours,
rate: i.rate,
amount: i.hours * i.rate,
})),
items: formData.items.map((i) => ({
date: i.date,
description: i.description,
hours: i.hours,
rate: i.rate,
amount: i.hours * i.rate,
})),
};
if (invoiceId && invoiceId !== "new" && invoiceId !== undefined)
await updateInvoice.mutateAsync({ id: invoiceId, ...payload });
@@ -454,18 +453,12 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const currentBusiness = businesses?.find(
(b) => b.id === formData.businessId,
);
const clientRate =
selectedClient && "defaultHourlyRate" in selectedClient
? selectedClient.defaultHourlyRate
: null;
const clientRate = getDefaultHourlyRate(selectedClient);
const businessRate =
currentBusiness &&
"defaultHourlyRate" in currentBusiness
? currentBusiness.defaultHourlyRate
: null;
getDefaultHourlyRate(currentBusiness);
updateField(
"defaultHourlyRate",
(clientRate ?? businessRate ?? 0) as number,
clientRate ?? businessRate ?? 0,
);
// Auto-fill currency from client
if (
@@ -473,10 +466,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
"currency" in selectedClient &&
selectedClient.currency
) {
updateField(
"currency",
selectedClient.currency as string,
);
updateField("currency", selectedClient.currency);
}
}}
>
@@ -772,6 +762,8 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
taxRate: formData.taxRate,
status: formData.status,
totalAmount: totals.total,
currency: formData.currency,
notes: formData.notes,
client: selectedClient
? {
name: selectedClient.name,
@@ -786,8 +778,11 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
: undefined,
items: formData.items.map((item) => ({
id: item.id,
date: item.date,
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.hours * item.rate,
})),
}}
/>
@@ -37,6 +37,8 @@ interface SendEmailDialogProps {
dueDate: Date;
status: string;
taxRate: number;
currency?: string | null;
notes?: string | null;
client?: {
name: string;
email: string | null;
@@ -47,8 +49,11 @@ interface SendEmailDialogProps {
};
items?: Array<{
id: string;
date?: Date;
description?: string;
hours: number;
rate: number;
amount?: number;
}>;
};
onEmailSent?: () => void;