feat: add PDF preview functionality and normalize email message handling

This commit is contained in:
2026-04-28 01:26:47 -04:00
parent 915ec103fc
commit bd3181fb9d
6 changed files with 293 additions and 229 deletions
+2 -2
View File
@@ -96,7 +96,7 @@ export function EmailComposer({
content: customMessage,
immediatelyRender: false,
onUpdate: ({ editor }) => {
onCustomMessageChange?.(editor.getHTML());
onCustomMessageChange?.(editor.isEmpty ? "" : editor.getHTML());
},
editorProps: {
attributes: {
@@ -109,7 +109,7 @@ export function EmailComposer({
// Update editor content when customMessage prop changes
useEffect(() => {
if (editor && customMessage !== undefined) {
const currentContent = editor.getHTML();
const currentContent = editor.isEmpty ? "" : editor.getHTML();
if (currentContent !== customMessage) {
editor.commands.setContent(customMessage);
}
+66 -56
View File
@@ -3,7 +3,6 @@
import * as React from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { PDFViewer } from "@react-pdf/renderer";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Label } from "~/components/ui/label";
@@ -22,7 +21,6 @@ import { PageHeader } from "~/components/layout/page-header";
import { InvoiceLineItems } from "./invoice-line-items";
import { InvoiceCalendarView } from "./invoice-calendar-view";
import { EmailPreview } from "./email-preview";
import { InvoicePDF } from "~/lib/pdf-export";
import { api } from "~/trpc/react";
import { toast } from "sonner";
import {
@@ -129,6 +127,7 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
const [initialized, setInitialized] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [activeTab, setActiveTab] = useState("details");
const [previewTab, setPreviewTab] = useState("pdf");
// Queries (Same as before)
const { data: clients, isLoading: loadingClients } =
@@ -222,6 +221,41 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
() => plainTextToHtml(formData.emailMessage.trim()),
[formData.emailMessage],
);
const pdfPreviewInput = React.useMemo(
() => ({
invoiceNumber: formData.invoiceNumber,
invoicePrefix: formData.invoicePrefix,
businessId: formData.businessId || "",
clientId: formData.clientId,
issueDate: formData.issueDate,
dueDate: formData.dueDate,
status: formData.status,
notes: formData.notes,
emailMessage: formData.emailMessage,
taxRate: formData.taxRate,
currency: formData.currency,
items: formData.items.map((item) => ({
date: item.date,
description: item.description || "Service",
hours: item.hours,
rate: item.rate,
})),
}),
[formData],
);
const { data: pdfPreview, isFetching: pdfPreviewLoading } =
api.invoices.previewPdf.useQuery(pdfPreviewInput, {
enabled:
activeTab === "preview" &&
previewTab === "pdf" &&
Boolean(formData.clientId) &&
formData.items.length > 0 &&
formData.items.every((item) => item.description.trim() !== ""),
refetchOnWindowFocus: false,
staleTime: 0,
});
const selectedClient = React.useMemo(
() => clients?.find((client) => client.id === formData.clientId),
[clients, formData.clientId],
@@ -777,7 +811,11 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
value="preview"
className="mt-6 focus-visible:outline-none"
>
<Tabs defaultValue="pdf" className="w-full">
<Tabs
value={previewTab}
onValueChange={setPreviewTab}
className="w-full"
>
<TabsList className="bg-muted grid h-auto w-full grid-cols-2 rounded-xl p-1">
<TabsTrigger
value="pdf"
@@ -802,60 +840,32 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
</CardHeader>
<CardContent className="p-0">
<div className="bg-muted/20 h-[760px] overflow-hidden border-t">
<PDFViewer
showToolbar
style={{ width: "100%", height: "100%", border: 0 }}
>
<InvoicePDF
invoice={{
invoiceNumber: formData.invoiceNumber,
invoicePrefix: formData.invoicePrefix,
issueDate: formData.issueDate,
dueDate: formData.dueDate,
status: formData.status,
totalAmount: totals.total,
taxRate: formData.taxRate,
currency: formData.currency,
notes: formData.notes,
client: selectedClient
? {
name: selectedClient.name,
email: selectedClient.email,
phone: selectedClient.phone,
addressLine1: selectedClient.addressLine1,
addressLine2: selectedClient.addressLine2,
city: selectedClient.city,
state: selectedClient.state,
postalCode: selectedClient.postalCode,
country: selectedClient.country,
}
: null,
business: selectedBusiness
? {
name: selectedBusiness.name,
nickname: selectedBusiness.nickname,
email: selectedBusiness.email,
phone: selectedBusiness.phone,
addressLine1: selectedBusiness.addressLine1,
addressLine2: selectedBusiness.addressLine2,
city: selectedBusiness.city,
state: selectedBusiness.state,
postalCode: selectedBusiness.postalCode,
country: selectedBusiness.country,
website: selectedBusiness.website,
taxId: selectedBusiness.taxId,
}
: null,
items: formData.items.map((item) => ({
date: item.date,
description: item.description,
hours: item.hours,
rate: item.rate,
amount: item.hours * item.rate,
})),
}}
{!formData.clientId ? (
<div className="text-muted-foreground flex h-full items-center justify-center p-6 text-center text-sm">
Select a client to generate the PDF preview.
</div>
) : formData.items.some(
(item) => item.description.trim() === "",
) ? (
<div className="text-muted-foreground flex h-full items-center justify-center p-6 text-center text-sm">
Add descriptions for all line items to generate the
PDF preview.
</div>
) : pdfPreviewLoading && !pdfPreview ? (
<div className="text-muted-foreground flex h-full items-center justify-center p-6 text-center text-sm">
Generating server PDF preview...
</div>
) : pdfPreview ? (
<iframe
title="Server-generated PDF preview"
src={`data:${pdfPreview.contentType};base64,${pdfPreview.base64}`}
className="h-full w-full border-0"
/>
</PDFViewer>
) : (
<div className="text-muted-foreground flex h-full items-center justify-center p-6 text-center text-sm">
PDF preview will appear here.
</div>
)}
</div>
</CardContent>
</Card>