From 3ac6e4d5b82417523078dc41c310a48149af10db Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Fri, 18 Jul 2025 20:18:43 -0400 Subject: [PATCH] Update Next.js to version 15.4.2 and refactor invoice pages - Upgrade Next.js and related packages for improved performance and security - Refactor invoice-related pages to streamline navigation and enhance user experience - Consolidate invoice editing and viewing functionality into a single page - Remove deprecated edit page and implement a new view page for invoices - Update links and routing for consistency across the dashboard --- bun.lock | 22 +- package.json | 2 +- .../[id]/_components/unified-invoice-page.tsx | 2 +- src/app/dashboard/invoices/[id]/edit/page.tsx | 11 - src/app/dashboard/invoices/[id]/page.tsx | 395 +--------- src/app/dashboard/invoices/[id]/view/page.tsx | 389 ++++++++++ .../_components/invoices-data-table.tsx | 2 +- src/app/dashboard/invoices/new/page.tsx | 719 ------------------ src/app/dashboard/page.tsx | 8 +- .../data/current-open-invoice-card.tsx | 2 +- src/components/data/invoice-list.tsx | 4 +- src/components/forms/address-form.tsx | 2 + src/components/forms/invoice-form.tsx | 680 ++++++----------- src/server/api/routers/invoices.ts | 78 +- 14 files changed, 727 insertions(+), 1589 deletions(-) delete mode 100644 src/app/dashboard/invoices/[id]/edit/page.tsx create mode 100644 src/app/dashboard/invoices/[id]/view/page.tsx delete mode 100644 src/app/dashboard/invoices/new/page.tsx diff --git a/bun.lock b/bun.lock index a2beae1..ae1d37f 100644 --- a/bun.lock +++ b/bun.lock @@ -41,7 +41,7 @@ "file-saver": "^2.0.5", "lucide": "^0.525.0", "lucide-react": "^0.525.0", - "next": "^15.4.1", + "next": "^15.4.2", "next-auth": "5.0.0-beta.25", "react": "^19.0.0", "react-day-picker": "^9.8.0", @@ -280,25 +280,25 @@ "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], - "@next/env": ["@next/env@15.4.1", "", {}, "sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A=="], + "@next/env": ["@next/env@15.4.2", "", {}, "sha512-kd7MvW3pAP7tmk1NaiX4yG15xb2l4gNhteKQxt3f+NGR22qwPymn9RBuv26QKfIKmfo6z2NpgU8W2RT0s0jlvg=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@15.3.5", "", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-BZwWPGfp9po/rAnJcwUBaM+yT/+yTWIkWdyDwc74G9jcfTrNrmsHe+hXHljV066YNdVs8cxROxX5IgMQGX190w=="], - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig=="], + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ovqjR8NjCBdBf1U+R/Gvn0RazTtXS9n6wqs84iFaCS1NHbw9ksVE4dfmsYcLoyUVd9BWE0bjkphOWrrz8uz/uw=="], - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw=="], + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-I8d4W7tPqbdbHRI4z1iBfaoJIBrEG4fnWKIe+Rj1vIucNZ5cEinfwkBt3RcDF00bFRZRDpvKuDjgMFD3OyRBnw=="], - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ=="], + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lvhz02dU3Ec5thzfQ2RCUeOFADjNkS/px1W7MBt7HMhf0/amMfT8Z/aXOwEA+cVWN7HSDRSUc8hHILoHmvajsg=="], - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g=="], + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-v+5PPfL8UP+KKHS3Mox7QMoeFdMlaV0zeNMIF7eLC4qTiVSO0RPNnK0nkBZSD5BEkkf//c+vI9s/iHxddCZchA=="], - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ=="], + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-PHLYOC9W2cu6I/JEKo77+LW4uPNvyEQiSkVRUQPsOIsf01PRr8PtPhwtz3XNnC9At8CrzPkzqQ9/kYDg4R4Inw=="], - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw=="], + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-lpmUF9FfLFns4JbTu+5aJGA8aR9dXaA12eoNe9CJbVkGib0FDiPa4kBGTwy0xDxKNGlv3bLDViyx1U+qafmuJQ=="], - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg=="], + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-aMjogoGnRepas0LQ/PBPsvvUzj+IoXw2IoDSEShEtrsu2toBiaxEWzOQuPZ8nie8+1iF7TA63S7rlp3YWAjNEg=="], - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ=="], + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-FxwauyexSFu78wEqR/+NB9MnqXVj6SxJKwcVs2CRjeSX/jBagDCgtR2W36PZUYm0WPgY1pQ3C1+nn7zSnwROuw=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1050,7 +1050,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "next": ["next@15.4.1", "", { "dependencies": { "@next/env": "15.4.1", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.1", "@next/swc-darwin-x64": "15.4.1", "@next/swc-linux-arm64-gnu": "15.4.1", "@next/swc-linux-arm64-musl": "15.4.1", "@next/swc-linux-x64-gnu": "15.4.1", "@next/swc-linux-x64-musl": "15.4.1", "@next/swc-win32-arm64-msvc": "15.4.1", "@next/swc-win32-x64-msvc": "15.4.1", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw=="], + "next": ["next@15.4.2", "", { "dependencies": { "@next/env": "15.4.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.2", "@next/swc-darwin-x64": "15.4.2", "@next/swc-linux-arm64-gnu": "15.4.2", "@next/swc-linux-arm64-musl": "15.4.2", "@next/swc-linux-x64-gnu": "15.4.2", "@next/swc-linux-x64-musl": "15.4.2", "@next/swc-win32-arm64-msvc": "15.4.2", "@next/swc-win32-x64-msvc": "15.4.2", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-oH1rmFso+84NIkocfuxaGKcXIjMUTmnzV2x0m8qsYtB4gD6iflLMESXt5XJ8cFgWMBei4v88rNr/j+peNg72XA=="], "next-auth": ["next-auth@5.0.0-beta.25", "", { "dependencies": { "@auth/core": "0.37.2" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0-0", "nodemailer": "^6.6.5", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog=="], diff --git a/package.json b/package.json index a537469..f27ba2f 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "file-saver": "^2.0.5", "lucide": "^0.525.0", "lucide-react": "^0.525.0", - "next": "^15.4.1", + "next": "^15.4.2", "next-auth": "5.0.0-beta.25", "react": "^19.0.0", "react-day-picker": "^9.8.0", diff --git a/src/app/dashboard/invoices/[id]/_components/unified-invoice-page.tsx b/src/app/dashboard/invoices/[id]/_components/unified-invoice-page.tsx index 3c1b2c3..7af3302 100644 --- a/src/app/dashboard/invoices/[id]/_components/unified-invoice-page.tsx +++ b/src/app/dashboard/invoices/[id]/_components/unified-invoice-page.tsx @@ -1,7 +1,7 @@ "use client"; import { InvoiceView } from "~/components/data/invoice-view"; -import { InvoiceForm } from "~/components/forms/invoice-form"; +import InvoiceForm from "~/components/forms/invoice-form"; interface UnifiedInvoicePageProps { invoiceId: string; diff --git a/src/app/dashboard/invoices/[id]/edit/page.tsx b/src/app/dashboard/invoices/[id]/edit/page.tsx deleted file mode 100644 index 6640a22..0000000 --- a/src/app/dashboard/invoices/[id]/edit/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client"; - -import { useParams } from "next/navigation"; -import { InvoiceForm } from "~/components/forms/invoice-form"; - -export default function EditInvoicePage() { - const params = useParams(); - const invoiceId = params.id as string; - - return ; -} diff --git a/src/app/dashboard/invoices/[id]/page.tsx b/src/app/dashboard/invoices/[id]/page.tsx index 44c8195..ecb84a3 100644 --- a/src/app/dashboard/invoices/[id]/page.tsx +++ b/src/app/dashboard/invoices/[id]/page.tsx @@ -1,389 +1,12 @@ -import { Suspense } from "react"; -import { notFound } from "next/navigation"; -import Link from "next/link"; -import { api, HydrateClient } from "~/trpc/server"; -import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { Button } from "~/components/ui/button"; -import { StatusBadge, type StatusType } from "~/components/data/status-badge"; -import { Separator } from "~/components/ui/separator"; -import { PageHeader } from "~/components/layout/page-header"; -import { PDFDownloadButton } from "./_components/pdf-download-button"; -import { SendInvoiceButton } from "./_components/send-invoice-button"; -import { InvoiceDetailsSkeleton } from "./_components/invoice-details-skeleton"; +"use client"; -import { - Building, - Edit, - FileText, - Mail, - MapPin, - Phone, - User, - AlertTriangle, - Check, -} from "lucide-react"; +import { useParams } from "next/navigation"; +import InvoiceForm from "~/components/forms/invoice-form"; -interface InvoicePageProps { - params: Promise<{ id: string }>; -} - -async function InvoiceContent({ invoiceId }: { invoiceId: string }) { - const invoice = await api.invoices.getById({ id: invoiceId }); - - if (!invoice) { - notFound(); - } - - const formatDate = (date: Date) => { - return new Intl.DateTimeFormat("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }).format(new Date(date)); - }; - - const formatCurrency = (amount: number) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - }).format(amount); - }; - - const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0); - const taxAmount = (subtotal * invoice.taxRate) / 100; - const total = subtotal + taxAmount; - const isOverdue = - new Date(invoice.dueDate) < new Date() && invoice.status !== "paid"; - - const getStatusType = (): StatusType => { - if (invoice.status === "paid") return "paid"; - if (invoice.status === "draft") return "draft"; - if (invoice.status === "overdue") return "overdue"; - if (invoice.status === "sent") { - return isOverdue ? "overdue" : "sent"; - } - return "draft"; - }; - - return ( -
- - - - - - {/* Content */} -
- {/* Left Column */} -
- {/* Invoice Header */} - - -
-
-
-
-

- {invoice.invoiceNumber} -

- -
-
-
- Issued {formatDate(invoice.issueDate)} -
-
- Due {formatDate(invoice.dueDate)} -
-
-
-
-

- Total Amount -

-

- {formatCurrency(total)} -

-
-
-
-
-
- - {/* Overdue Alert */} - {isOverdue && ( - - -
- -
-

Invoice Overdue

-

- {Math.ceil( - (new Date().getTime() - - new Date(invoice.dueDate).getTime()) / - (1000 * 60 * 60 * 24), - )}{" "} - days past due date -

-
-
-
-
- )} - - {/* Client & Business Info */} -
- {/* Client Information */} - - - - - Bill To - - - -
-

- {invoice.client.name} -

-
- -
- {invoice.client.email && ( -
-
- -
- - {invoice.client.email} - -
- )} - - {invoice.client.phone && ( -
-
- -
- {invoice.client.phone} -
- )} - - {(invoice.client.addressLine1 ?? invoice.client.city) && ( -
-
- -
-
- {invoice.client.addressLine1 && ( -
{invoice.client.addressLine1}
- )} - {invoice.client.addressLine2 && ( -
{invoice.client.addressLine2}
- )} - {(invoice.client.city ?? - invoice.client.state ?? - invoice.client.postalCode) && ( -
- {[ - invoice.client.city, - invoice.client.state, - invoice.client.postalCode, - ] - .filter(Boolean) - .join(", ")} -
- )} - {invoice.client.country && ( -
{invoice.client.country}
- )} -
-
- )} -
-
-
- - {/* Business Information */} - {invoice.business && ( - - - - - From - - - -
-

- {invoice.business.name} -

-
- -
- {invoice.business.email && ( -
-
- -
- - {invoice.business.email} - -
- )} - - {invoice.business.phone && ( -
-
- -
- - {invoice.business.phone} - -
- )} -
-
-
- )} -
- - {/* Invoice Items */} - - - - - Invoice Items - - - - {invoice.items.map((item) => ( - - -
-
-

- {item.description} -

-
- - {formatDate(item.date).replace(/ /g, "\u00A0")} - - - {item.hours.toString().replace(/ /g, "\u00A0")} -  hours - - - @ ${item.rate}/hr - -
-
-
-

- {formatCurrency(item.amount)} -

-
-
-
-
- ))} - - {/* Totals */} -
-
-
- Subtotal: - - {formatCurrency(subtotal)} - -
- {invoice.taxRate > 0 && ( -
- - Tax ({invoice.taxRate}%): - - - {formatCurrency(taxAmount)} - -
- )} - -
- Total: - - {formatCurrency(total)} - -
-
-
-
-
- - {/* Notes */} - {invoice.notes && ( - - - Notes - - -

- {invoice.notes} -

-
-
- )} -
- - {/* Right Column - Actions */} -
- - - - - Actions - - - - - - {invoice.items && invoice.client && ( - - )} - - {invoice.status === "draft" && ( - - )} - - -
-
-
- ); -} - -export default async function InvoicePage({ params }: InvoicePageProps) { - const { id } = await params; - - return ( - - }> - - - - ); +export default function InvoiceFormPage() { + const params = useParams(); + const id = params.id as string; + + // Pass the actual id, let the form component handle the logic + return ; } diff --git a/src/app/dashboard/invoices/[id]/view/page.tsx b/src/app/dashboard/invoices/[id]/view/page.tsx new file mode 100644 index 0000000..b4e959c --- /dev/null +++ b/src/app/dashboard/invoices/[id]/view/page.tsx @@ -0,0 +1,389 @@ +import { Suspense } from "react"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { api, HydrateClient } from "~/trpc/server"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Button } from "~/components/ui/button"; +import { StatusBadge, type StatusType } from "~/components/data/status-badge"; +import { Separator } from "~/components/ui/separator"; +import { PageHeader } from "~/components/layout/page-header"; +import { PDFDownloadButton } from "../_components/pdf-download-button"; +import { SendInvoiceButton } from "../_components/send-invoice-button"; +import { InvoiceDetailsSkeleton } from "../_components/invoice-details-skeleton"; + +import { + Building, + Edit, + FileText, + Mail, + MapPin, + Phone, + User, + AlertTriangle, + Check, +} from "lucide-react"; + +interface InvoiceViewPageProps { + params: Promise<{ id: string }>; +} + +async function InvoiceViewContent({ invoiceId }: { invoiceId: string }) { + const invoice = await api.invoices.getById({ id: invoiceId }); + + if (!invoice) { + notFound(); + } + + const formatDate = (date: Date) => { + return new Intl.DateTimeFormat("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }).format(new Date(date)); + }; + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + }).format(amount); + }; + + const subtotal = invoice.items.reduce((sum, item) => sum + item.amount, 0); + const taxAmount = (subtotal * invoice.taxRate) / 100; + const total = subtotal + taxAmount; + const isOverdue = + new Date(invoice.dueDate) < new Date() && invoice.status !== "paid"; + + const getStatusType = (): StatusType => { + if (invoice.status === "paid") return "paid"; + if (invoice.status === "draft") return "draft"; + if (invoice.status === "overdue") return "overdue"; + if (invoice.status === "sent") { + return isOverdue ? "overdue" : "sent"; + } + return "draft"; + }; + + return ( +
+ + + + + + {/* Content */} +
+ {/* Left Column */} +
+ {/* Invoice Header */} + + +
+
+
+
+

+ {invoice.invoiceNumber} +

+ +
+
+
+ Issued {formatDate(invoice.issueDate)} +
+
+ Due {formatDate(invoice.dueDate)} +
+
+
+
+

+ Total Amount +

+

+ {formatCurrency(total)} +

+
+
+
+
+
+ + {/* Overdue Alert */} + {isOverdue && ( + + +
+ +
+

Invoice Overdue

+

+ {Math.ceil( + (new Date().getTime() - + new Date(invoice.dueDate).getTime()) / + (1000 * 60 * 60 * 24), + )}{" "} + days past due date +

+
+
+
+
+ )} + + {/* Client & Business Info */} +
+ {/* Client Information */} + + + + + Bill To + + + +
+

+ {invoice.client.name} +

+
+ +
+ {invoice.client.email && ( +
+
+ +
+ + {invoice.client.email} + +
+ )} + + {invoice.client.phone && ( +
+
+ +
+ {invoice.client.phone} +
+ )} + + {(invoice.client.addressLine1 ?? invoice.client.city) && ( +
+
+ +
+
+ {invoice.client.addressLine1 && ( +
{invoice.client.addressLine1}
+ )} + {invoice.client.addressLine2 && ( +
{invoice.client.addressLine2}
+ )} + {(invoice.client.city ?? + invoice.client.state ?? + invoice.client.postalCode) && ( +
+ {[ + invoice.client.city, + invoice.client.state, + invoice.client.postalCode, + ] + .filter(Boolean) + .join(", ")} +
+ )} + {invoice.client.country && ( +
{invoice.client.country}
+ )} +
+
+ )} +
+
+
+ + {/* Business Information */} + {invoice.business && ( + + + + + From + + + +
+

+ {invoice.business.name} +

+
+ +
+ {invoice.business.email && ( +
+
+ +
+ + {invoice.business.email} + +
+ )} + + {invoice.business.phone && ( +
+
+ +
+ + {invoice.business.phone} + +
+ )} +
+
+
+ )} +
+ + {/* Invoice Items */} + + + + + Invoice Items + + + + {invoice.items.map((item) => ( + + +
+
+

+ {item.description} +

+
+ + {formatDate(item.date).replace(/ /g, "\u00A0")} + + + {item.hours.toString().replace(/ /g, "\u00A0")} +  hours + + + @ ${item.rate}/hr + +
+
+
+

+ {formatCurrency(item.amount)} +

+
+
+
+
+ ))} + + {/* Totals */} +
+
+
+ Subtotal: + + {formatCurrency(subtotal)} + +
+ {invoice.taxRate > 0 && ( +
+ + Tax ({invoice.taxRate}%): + + + {formatCurrency(taxAmount)} + +
+ )} + +
+ Total: + + {formatCurrency(total)} + +
+
+
+
+
+ + {/* Notes */} + {invoice.notes && ( + + + Notes + + +

+ {invoice.notes} +

+
+
+ )} +
+ + {/* Right Column - Actions */} +
+ + + + + Actions + + + + + + {invoice.items && invoice.client && ( + + )} + + {invoice.status === "draft" && ( + + )} + + +
+
+
+ ); +} + +export default async function InvoiceViewPage({ params }: InvoiceViewPageProps) { + const { id } = await params; + + return ( + + }> + + + + ); +} diff --git a/src/app/dashboard/invoices/_components/invoices-data-table.tsx b/src/app/dashboard/invoices/_components/invoices-data-table.tsx index ee566df..fc86d8a 100644 --- a/src/app/dashboard/invoices/_components/invoices-data-table.tsx +++ b/src/app/dashboard/invoices/_components/invoices-data-table.tsx @@ -181,7 +181,7 @@ export function InvoicesDataTable({ invoices }: InvoicesDataTableProps) { - + - - - - Delete Item - - Are you sure you want to delete this line item? This action - cannot be undone. - - - - Cancel - onDelete(index)} - className="btn-danger" - > - Delete - - - - - - - {/* Description */} -